1(function () { 2 var still = document.documentElement.classList.contains('poster') || window.PUI.REDUCED; 3 /* Hero (mockup): threats arrive from the left and meet the agent's shield, a curved glowing membrane with 4 the ThreatResponder logo bent along it; what the agent does fans out on the right. The lines sway, 5 particles stream along them all the time, and every second or two one threat is carried in, flares on 6 the shield, makes the logo pulse where it hit, and flows on to the capabilities that answer it. 7 Membrane keeps a standing list of threats with swaying lines; Waves leaves the membrane alone and 8 sends threats in as blocks (campaigns). */ 9 (function shieldHero() { 10 const mode = window.__heroMode, host = document.getElementById('tsh'); 11 if (!host || ['layers', 'console'].indexOf(mode) < 0) return; 12 /* Quiet = Waves, one notch fainter everywhere, with the capabilities a little larger and further apart */ 13 /* Layers / Console / Console · colour: Waves at the Quiet level on the left, a different scene on the right */ 14 const right = ['layers', 'console', 'console2'].indexOf(mode) >= 0 ? mode : 'caps'; 15 const quiet = mode === 'quiet' || right !== 'caps', strike = mode === 'waves' || quiet, capGap = quiet ? 97 : 88, capTop = quiet ? 80 : 102, capR = quiet ? 23 : 21, capTx = quiet ? 38 : 34; 16 host.classList.add('tsv', 'v-membrane', 'v-' + mode); 17 if (quiet) host.classList.add('v-quiet'); 18 let upd = null; 19 const hero = document.getElementById('hero'), text = hero.querySelector('.hgrid > div'), shot = document.getElementById('heroShot'); 20 const NS = 'http://www.w3.org/2000/svg'; 21 const IC = { 22 mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3.5 7l8.5 6 8.5-6"/>', 23 lock: '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/>', 24 code: '<path d="M8 8l-4 4 4 4M16 8l4 4-4 4M13.5 5l-3 14"/>', 25 bug: '<path d="M8 8h8v6a4 4 0 0 1-8 0z"/><path d="M12 4v4M4 12h4M16 12h4M5 18l3-2M19 18l-3-2M5 6l3 2M19 6l-3 2"/>', 26 user: '<circle cx="12" cy="8" r="4"/><path d="M4 21c1.5-4 4.5-6 8-6s6.5 2 8 6"/>', 27 lateral: '<circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="18" r="2.5"/><circle cx="12" cy="6" r="2.5"/><path d="M10.8 8.2L7.2 15.8M13.2 8.2l3.6 7.6M8.5 18h7"/>', 28 beacon: '<circle cx="12" cy="12" r="2"/><path d="M8.5 15.5a5 5 0 0 1 0-7M15.5 8.5a5 5 0 0 1 0 7M5.6 18.4a9 9 0 0 1 0-12.8M18.4 5.6a9 9 0 0 1 0 12.8"/>', 29 search: '<circle cx="11" cy="11" r="6.5"/><path d="M16 16l4.5 4.5"/>', 30 mitre: '<circle cx="12" cy="12" r="2.5"/><circle cx="5" cy="5" r="2"/><circle cx="19" cy="5" r="2"/><circle cx="5" cy="19" r="2"/><circle cx="19" cy="19" r="2"/><path d="M6.5 6.5l3.7 3.7M17.5 6.5l-3.7 3.7M6.5 17.5l3.7-3.7M17.5 17.5l-3.7-3.7"/>', 31 doc: '<path d="M7 3h7l5 5v13H7z"/><path d="M14 3v5h5M10 13h6M10 17h6"/>', 32 shield: '<path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/>', 33 cross: '<circle cx="12" cy="12" r="7"/><path d="M12 2v5M12 17v5M2 12h5M17 12h5"/><circle cx="12" cy="12" r="1.2"/>', 34 bars: '<path d="M4 20V11M10 20V5M16 20v-6M2.5 20.5h19"/>' 35 }; 36 const THREATS = [ 37 { ic: 'mail', n: 'Phishing', s: 'MALICIOUS EMAIL', caps: ['det', 'alert', 'contain'] }, 38 { ic: 'lock', n: 'Ransomware', s: 'ENCRYPTION ATTEMPT', caps: ['det', 'mitre', 'contain'] }, 39 { ic: 'code', n: 'Malicious script', s: 'POWERSHELL · BASH', caps: ['det', 'mitre', 'hunt'] }, 40 { ic: 'bug', n: 'Exploit', s: 'ZERO-DAY · REMOTE', caps: ['det', 'hunt', 'fx'] }, 41 { ic: 'user', n: 'Credential theft', s: 'STOLEN CREDENTIALS', caps: ['det', 'mitre', 'alert'] }, 42 { ic: 'lateral', n: 'Lateral movement', s: 'REMOTE LOGONS', caps: ['det', 'hunt', 'fx'] }, 43 { ic: 'beacon', n: 'C2 beacon', s: 'OUTBOUND CONNECTION', caps: ['det', 'contain', 'hunt'] } 44 ]; 45 const CAPS = [ 46 { id: 'det', ic: 'search', n: 'Threat detection', s: 'FOUR ENGINES · ONE VERDICT' }, 47 { id: 'mitre', ic: 'mitre', n: 'MITRE ATT&CK mapping', s: 'EVERY STEP, A TECHNIQUE' }, 48 { id: 'alert', ic: 'doc', n: 'Custom alert rules', s: 'ALERT · NOTIFY · INTEGRATE' }, 49 { id: 'contain', ic: 'shield', n: 'Containment', s: 'CONTAIN · KILL · QUARANTINE' }, 50 { id: 'hunt', ic: 'cross', n: 'Threat hunting', s: 'ONE QUERY, EVERY ENDPOINT' }, 51 { id: 'fx', ic: 'bars', n: 'Forensics', s: 'TIMELINE · EVIDENCE · REPORT' } 52 ]; 53 const OX = 900, OY = 322, RIM = 432, capX = 852; 54 function ax(y, r) { return OX - Math.sqrt(Math.max(0, r * r - (y - OY) * (y - OY))); } 55 function pts(r, y0, y1) { const a = []; for (let y = y0; y <= y1; y += 8) a.push(ax(y, r).toFixed(1) + ',' + y); return a; } 56 function arc(r, y0, y1) { return 'M' + pts(r, y0, y1).join(' L'); } 57 function esc(v) { return String(v).replace(/&/g, '&').replace(/</g, '<'); } 58 function icon(n, x, y, s) { return '<svg class="ic" x="' + x + '" y="' + y + '" width="' + s + '" height="' + s + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">' + IC[n] + '</svg>'; } 59 function badge(cx, cy, r, ic) { return '<circle class="ring" cx="' + cx + '" cy="' + cy + '" r="' + r + '"/>' + icon(ic, cx - 10, cy - 10, 20); }
60 /* a line = four control points; it sways by moving the two inner ones */ 61 function line(p0, p1, p2, p3, amp) { return { b: [p0, p1, p2, p3], ph: Math.random() * 6.3, amp: amp, sp: .8 + Math.random() * .5 }; } 62 function cur(L, t) { const a = Math.sin(t * .00055 * L.sp + L.ph) * L.amp, b = Math.cos(t * .00047 * L.sp + L.ph * 1.7) * L.amp; return [L.b[0], [L.b[1][0], L.b[1][1] + a], [L.b[2][0], L.b[2][1] + b], L.b[3]]; } 63 function bez(P, u) { const v = 1 - u, A = v * v * v, B = 3 * v * v * u, C = 3 * v * u * u, D = u * u * u; return [A * P[0][0] + B * P[1][0] + C * P[2][0] + D * P[3][0], A * P[0][1] + B * P[1][1] + C * P[2][1] + D * P[3][1]]; } 64 function dOf(P) { return 'M' + P[0][0].toFixed(1) + ',' + P[0][1].toFixed(1) + ' C' + P[1][0].toFixed(1) + ',' + P[1][1].toFixed(1) + ' ' + P[2][0].toFixed(1) + ',' + P[2][1].toFixed(1) + ' ' + P[3][0].toFixed(1) + ',' + P[3][1].toFixed(1); } 65 66 const INNER = 384, LOGO_R = 408, LOGO_K = .92; 67 let s = '<svg viewBox="0 0 1100 640" preserveAspectRatio="xMinYMid meet" xmlns="http://www.w3.org/2000/svg"><defs>' + 68 '<pattern id="tsvMesh" width="16" height="27.7" patternUnits="userSpaceOnUse"><path d="M8,0 L16,4.62 L16,13.86 L8,18.48 L0,13.86 L0,4.62 Z M8,18.48 L8,27.7" fill="none" class="mesh-p"/></pattern>' + 69 '<clipPath id="tsvBand"><path d="M' + pts(RIM, 24, 620).concat(pts(INNER, 24, 620).reverse()).join(' L') + ' Z"/></clipPath>' + 70 '<linearGradient id="tsvFadeG" gradientUnits="userSpaceOnUse" x1="0" y1="20" x2="0" y2="624"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".24" stop-color="#fff"/><stop offset=".76" stop-color="#fff"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>' + 71 '<mask id="tsvFade" maskUnits="userSpaceOnUse" x="300" y="0" width="360" height="640"><rect x="300" y="0" width="360" height="640" fill="url(#tsvFadeG)"/></mask>' + 72 '<linearGradient id="tsvBandG" gradientUnits="userSpaceOnUse" x1="' + (OX - RIM) + '" y1="0" x2="' + (OX - INNER + 30) + '" y2="0"><stop offset="0" stop-color="#9dbcff" stop-opacity=".22"/><stop offset=".45" stop-color="#6f95ff" stop-opacity=".08"/><stop offset="1" stop-color="#6f95ff" stop-opacity="0"/></linearGradient>' + 73 '<radialGradient id="tsvHot"><stop offset="0" stop-color="#d6e4ff" stop-opacity=".45"/><stop offset="1" stop-color="#d6e4ff" stop-opacity="0"/></radialGradient>' + 74 '<filter id="tsvBlur" x="-60%" y="-10%" width="220%" height="120%"><feGaussianBlur stdDeviation="5"/></filter>' + 75 '<radialGradient id="tsvFlare"><stop offset="0" stop-color="#f4f7ff" stop-opacity=".9"/><stop offset=".35" stop-color="#b9ccff" stop-opacity=".35"/><stop offset="1" stop-color="#b9ccff" stop-opacity="0"/></radialGradient><linearGradient id="tsvLayer" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#9fc4ff" stop-opacity=".085"/><stop offset="1" stop-color="#6597ff" stop-opacity=".035"/></linearGradient></defs>'; 76 77 /* the membrane, layered like the reference: halo rings toward the threats, a glowing rim, a translucent 78 band with a hex mesh and the logo; it fades out at the top and bottom */ 79 s += '<g mask="url(#tsvFade)">' + 80 '<path class="sh-halo" d="' + arc(446, 24, 620) + '"/>' + 81 [[520, .03], [494, .05], [470, .08], [452, .14]].map(function (a) { return '<path class="sh-ring" style="stroke-opacity:' + a[1] + '" d="' + arc(a[0], 24, 620) + '"/>'; }).join('') + 82 '<path d="M' + pts(RIM, 24, 620).concat(pts(INNER, 24, 620).reverse()).join(' L') + ' Z" fill="url(#tsvBandG)"/>' + 83 '<rect x="360" y="20" width="200" height="604" fill="url(#tsvMesh)" clip-path="url(#tsvBand)" class="mesh"/>' + 84 '<ellipse cx="' + (OX - RIM + 4) + '" cy="' + OY + '" rx="34" ry="170" fill="url(#tsvHot)" class="hot"/>' + 85 '<path class="sh-rimglow" filter="url(#tsvBlur)" d="' + arc(RIM, 24, 620) + '"/><path class="sh-main" d="' + arc(RIM, 24, 620) + '"/><path class="sh-in" d="' + arc(INNER, 24, 620) + '"/>' + 86 '<g class="specks"></g></g><g class="mlogo"></g>'; 87 88 /* threats on the left; each has a swaying dashed line to the rim (Membrane only; Waves sends them in blocks) */
89 if (!strike) THREATS.forEach(function (t, i) { 90 const y = 92 + i * 76, bx = 252, iy = OY + (y - OY) * .5, ix = ax(iy, RIM + 2) - 2, x0 = bx + 24; 91 t.ix = ix; t.iy = iy; 92 t.L = line([x0, y], [x0 + 95, y + (i % 2 ? 10 : -10)], [ix - 95, iy], [ix, iy], 7 + Math.random() * 8); 93 s += '<path class="tl" data-l="t' + i + '"/><circle class="tend" cx="' + ix + '" cy="' + iy + '" r="2"/>' + 94 '<g class="th" data-t="' + i + '">' + badge(bx, y, 21, t.ic) + '<text class="ttl" x="' + (bx - 34) + '" y="' + (y - 2) + '" text-anchor="end">' + esc(t.n) + '</text><text class="tsb" x="' + (bx - 34) + '" y="' + (y + 13) + '" text-anchor="end">' + t.s + '</text></g>'; 95 }); 96 97 98 /* Right-hand scenes for Layers and Console (Waves on the left, Quiet level). A scene gives the fibre 99 targets, which of them a threat reaches, and what happens when a signal arrives */ 100 const TX = [ 101 { t: 'T1566', act: 'attachment quarantined' }, { t: 'T1486', act: 'process killed' }, { t: 'T1059', act: 'process tree killed' }, 102 { t: 'T1203', act: 'operation blocked' }, { t: 'T1003', act: 'endpoint contained' }, { t: 'T1021', act: 'endpoint contained' }, { t: 'T1071', act: 'connection closed' } 103 ]; 104 /* the product's own response actions (Resolve / Validate / detection-rule actions), rotated per threat and 105 weighted so Quarantine and Kill process come up most often, Contain endpoint less */ 106 const RESP = [['Quarantine', 'Quarantine', 'Clean'], ['Kill process', 'Revert', 'Quarantine'], ['Kill process', 'Quarantine', 'Kill process'], ['Block', 'Kill process', 'Kill process'], 107 ['Kill process', 'Contain endpoint', 'Quarantine'], ['Contain endpoint', 'Kill process', 'Quarantine'], ['Close connection', 'Kill process', 'Quarantine']]; 108 const respN = RESP.map(function () { return 0; }); 109 function respFor(i) { return RESP[i][respN[i]++ % RESP[i].length]; } 110 function buildRight(kind) { 111 const R = { targets: [], svg: '' }, lx = function (y) { return ax(OY + (y - OY) * .3, INNER) + 6; }, ly = function (y) { return OY + (y - OY) * .3; }; 112 function fibres(y, to, amp) { return [-5, 0, 5].map(function (o) { return line([lx(y), ly(y) + o], [lx(y) + 70, ly(y) + o * 1.5], [to[0] - 80, to[1] + o * .5], to, amp || 5 + Math.random() * 5); }); } 113 if (kind === 'layers') { 114 /* three glass slabs in depth, after the capability study: a skewed plane with a thickness edge, a faint 115 grid, corner dots, the layer's icon and name, two small record slabs, and a card in front. Detect sits 116 on top, investigate at the bottom. No pulses travel to them: after a hit the layers simply light up a 117 little, one threat at a time, paced, so each detection can be read */ 118 const K = 1.1, X0 = 598, LV = [ 119 { id: 'investigate', y: 448, name: 'INVESTIGATE', ic: 'bars', title: 'Hunting + forensics', sub: 'Timeline · searchable evidence' }, 120 { id: 'respond', y: 324, name: 'RESPOND', ic: 'shield', title: '', sub: '' }, 121 { id: 'detect', y: 200, name: 'DETECT', ic: 'search', title: 'Threat detection', sub: 'Custom rules · ATT&CK mapping' }]; 122 const top = LV[2].y, bot = LV[0].y; 123 R.svg += '<path class="ly-v" d="M' + X0 + ',' + top + 'V' + (bot + 10 * K) + 'M' + (X0 + 424 * K) + ',' + (top + 3 * K) + 'V' + (bot + 13 * K) + 'M' + (X0 + 214 * K) + ',' + (top + 78 * K) + 'V' + (bot + 88 * K) + '"/>'; 124 LV.forEach(function (l) { 125 const plane = 'M0 0 208-63 424 3 214 78Z'; 126 let body = '<text class="tsb ly-d" x="44" y="37">' + esc(l.sub) + '</text>'; 127 if (l.id === 'detect') body += '<g class="dx-pos"><g class="dx-pill"><rect height="14" rx="7"/><text x="7" y="10"></text></g></g>'; 128 if (l.id === 'respond') body = '<text class="ttl ra" x="44" y="30">Quarantine</text>'; 129 R.svg += '<g class="ly" data-c="' + l.id + '" transform="translate(' + X0 + ' ' + l.y + ')"><g transform="scale(' + K + ')">' + 130 '<path class="ly-p" d="' + plane + '"/><path class="ly-e" d="M0 0v10l214 78 210-76V3L214 78Z"/><path class="ly-hi" d="' + plane + '"/>' + 131 [.25, .5, .75].map(function (f) { return '<path class="ly-g" d="M' + (208 * f) + ' ' + (-63 * f) + 'l216 66M' + (216 * f) + ' ' + (66 * f) + 'l208-63"/>'; }).join('') + 132 '<circle class="ly-dot" r="3"/><circle class="ly-dot" cx="424" cy="3" r="3"/><circle class="ly-dot" cx="214" cy="78" r="3"/>' + 133 '<g class="ly-rec"><path d="m125-3 39-12 46 14-39 13Z m54 17 39-12 46 14-39 13Z"/><path class="ln" d="m142-1 20-6m30 21 20-6"/></g>' + 134 icon(l.ic, 306, -6, 22) + '<text class="ly-n" x="317" y="36" text-anchor="middle">' + l.name + '</text></g>' + 135 '<g class="ly-c" transform="translate(' + (28 * K) + ' ' + (37 * K) + ')"><rect width="214" height="50" rx="7"/>' + icon(l.ic, 11, 14, 21) + 136 (l.title ? '<text class="ttl" x="44" y="22">' + esc(l.title) + '</text>' : '') + body + '</g></g>'; 137 R.targets.unshift({ id: l.id, sub: l.sub, F: fibres(l.y, [X0 - 5, l.y]) }); 138 }); 139 R.svg += '<text class="zn" x="' + (X0 + 214 * K) + '" y="590">ALWAYS-ON VISIBILITY · COORDINATED RESPONSE</text>'; 140 R.noPulse = true; 141 R.pick = function () { return []; }; 142 const queue = [], lit = {}; 143 let nextOut = 0, arrivals = 0; 144 R.init = function () {
145 R.targets.forEach(function (tg) { tg.el = host.querySelector('.ly[data-c="' + tg.id + '"]'); tg.d = tg.el.querySelector('.ly-d'); lit[tg.id] = 0; }); 146 R.pill = host.querySelector('.dx-pill'); R.ra = host.querySelector('.ra'); 147 }; 148 /* every arriving threat queues one detection; every third also queues a custom-alert entry */ 149 R.arrive = function (id, th) { 150 if (queue.length < 4) queue.push({ th: th, i: THREATS.indexOf(th) }); 151 if (++arrivals % 3 === 0 && queue.length < 5) queue.push({ alert: true, th: th }); 152 }; 153 const tgt = function (id) { return R.targets.find(function (x) { return x.id === id; }); }; 154 function show(item, t) { 155 const det = tgt('detect'), pill = R.pill, pt = pill.querySelector('text'), pr = pill.querySelector('rect'); 156 det.d.textContent = item.alert ? 'Custom alert rule fired' : item.th.n; 157 pt.textContent = item.alert ? 'SLACK' : TX[item.i].t; 158 const x = 44 + det.d.getComputedTextLength() + 7, w = pt.getComputedTextLength() + 14; 159 pill.parentNode.setAttribute('transform', 'translate(' + x.toFixed(1) + ' 27)'); pr.setAttribute('width', w.toFixed(1)); 160 pill.classList.remove('pop'); void pill.getBBox(); pill.classList.add('pop'); 161 pill.classList.toggle('alert', !!item.alert); 162 lit.detect = t + 1500; det.until = t + 1700; 163 if (!item.alert) { 164 R.pending = [[t + 550, 'respond', respFor(item.i)], [t + 1100, 'investigate']]; 165 } 166 } 167 R.step = function (t) { 168 if (queue.length && t >= nextOut) { show(queue.shift(), t); nextOut = t + 1800; } 169 (R.pending || []).slice().forEach(function (p) { 170 if (t < p[0]) return; 171 R.pending.splice(R.pending.indexOf(p), 1); 172 lit[p[1]] = t + 1400; 173 /* the response action is the card's title, swapped in turn (Quarantine, Kill process, Contain endpointâ¦) */ 174 if (p[1] === 'respond') { R.ra.textContent = p[2]; R.ra.classList.remove('swap'); void R.ra.getBBox(); R.ra.classList.add('swap'); } 175 if (p[1] === 'investigate') { const inv = tgt('investigate'); inv.d.textContent = 'Timeline · evidence preserved'; inv.until = t + 1600; } 176 }); 177 R.targets.forEach(function (tg) { 178 tg.el.classList.toggle('on', t < lit[tg.id]); 179 if (tg.until && t > tg.until) { tg.until = 0; if (tg.id === 'detect') { tg.d.textContent = tg.sub; R.pill.classList.remove('pop'); } else if (tg.d) tg.d.textContent = tg.sub; } 180 }); 181 }; 182 return R; 183 } 184 /* the console: a wireframe of the Overview, notifications arrive as each threat is handled */ 185 const CX = 590, CY = 96, CW = 480, CH = 470, color = false; 186 const KPI = [['Threats', 41, 'crit'], ['Vulnerabilities', 61, 'warn'], ['Identity', 27, 'info'], ['Endpoints', '42/51', 'ok']]; 187 R.svg += '<g class="cs' + (color ? ' col' : '') + '" data-c="console"><rect class="cs-w" x="' + CX + '" y="' + CY + '" width="' + CW + '" height="' + CH + '" rx="14"/>' + 188 '<path class="cs-l" d="M' + CX + ',' + (CY + 36) + 'H' + (CX + CW) + 'M' + (CX + 44) + ',' + (CY + 36) + 'V' + (CY + CH) + '"/>' + 189 [0, 1, 2].map(function (j) { return '<circle class="cs-dot" cx="' + (CX + 18 + j * 12) + '" cy="' + (CY + 18) + '" r="3.2"/>'; }).join('') + 190 '<text class="cs-t" x="' + (CX + 60) + '" y="' + (CY + 22) + '">ThreatResponder Management Console</text><text class="cs-t dim" x="' + (CX + CW - 16) + '" y="' + (CY + 22) + '" text-anchor="end">All companies · last 30 days</text>' + 191 ['search', 'shield', 'mitre', 'bars', 'doc'].map(function (ic, j) { return icon(ic, CX + 13, CY + 54 + j * 42, 18); }).join('') + 192 KPI.map(function (k, j) { const x = CX + 60 + j * 102; return '<g class="kpi ' + k[2] + '"><rect x="' + x + '" y="' + (CY + 52) + '" width="94" height="58" rx="7"/><rect class="kb" x="' + x + '" y="' + (CY + 52) + '" width="3" height="58" rx="1.5"/><text class="kl" x="' + (x + 12) + '" y="' + (CY + 70) + '">' + k[0] + '</text><text class="kv" x="' + (x + 12) + '" y="' + (CY + 97) + '"' + (j === 0 ? ' data-kpi="threats"' : '') + '>
192' + k[1] + '</text></g>'; }).join('') + 193 '<rect class="cs-b" x="' + (CX + 60) + '" y="' + (CY + 124) + '" width="' + (CW - 76) + '" height="130" rx="8"/><text class="cs-h" x="' + (CX + 74) + '" y="' + (CY + 144) + '">WHAT ARRIVED · LAST 14 DAYS</text>' + 194 Array.from({ length: 14 }, function (_, j) { const x = CX + 76 + j * 27, h = 20 + ((j * 37) % 55); return '<rect class="bar' + (j % 3 === 0 ? ' b2' : j % 4 === 1 ? ' b3' : '') + '" data-bar="' + j + '" x="' + x + '" y="' + (CY + 240 - h) + '" width="16" height="' + h + '" rx="2"/>'; }).join('') + 195 '<rect class="cs-b" x="' + (CX + 60) + '" y="' + (CY + 268) + '" width="' + (CW - 76) + '" height="186" rx="8"/><text class="cs-h" x="' + (CX + 74) + '" y="' + (CY + 288) + '">NEEDS YOUR ATTENTION</text><g class="rows"></g>' + 196 '<g class="toasts"></g></g>'; 197 /* no connectors: the hit is handed straight to the console, which answers with a calm notification */ 198 R.targets.push({ id: 'console', F: [] }); 199 R.noPulse = true; 200 R.pick = function () { return []; }; 201 const rows = [], toasts = []; 202 let threats = 41; 203 R.init = function () { 204 R.rowsG = host.querySelector('.cs .rows'); R.toastG = host.querySelector('.cs .toasts'); R.kpi = host.querySelector('[data-kpi="threats"]'); R.bars = host.querySelectorAll('.cs [data-bar]'); 205 [['Ransomware behaviour stopped', 'FIN-SRV-02 · contained', 'crit'], ['Credential dumping via LSASS', 'LAPTOP-EAF574 · contained', 'crit'], ['Unusual service-ticket burst', 'DEV-WKS-07 · reviewing', 'warn']].forEach(function (r) { addRow(r[0], r[1], r[2], -1e9); }); 206 }; 207 function el(tag, a, parent) { const e = document.createElementNS(NS, tag); Object.keys(a).forEach(function (k) { e.setAttribute(k, a[k]); }); parent.appendChild(e); return e; } 208 function addRow(title, sub, sev, t) { 209 const g = el('g', { class: 'row ' + sev }, R.rowsG); 210 g.innerHTML = '<rect width="' + (CW - 104) + '" height="42" rx="6"/><rect class="rs" width="3" height="42" rx="1.5"/><text class="rt" x="14" y="17">' + esc(title) + '</text><text class="rd" x="14" y="32">' + esc(sub) + '</text>'; 211 rows.unshift({ g: g, t0: t, y: CY + 292 }); 212 while (rows.length > 4) rows.pop().g.remove(); 213 } 214 function toast(title, sub, sev, ic, t) { 215 const g = el('g', { class: 'toast ' + sev }, R.toastG); 216 g.innerHTML = '<rect width="224" height="46" rx="8"/><rect class="rs" width="3" height="46" rx="1.5"/>' + icon(ic, 12, 14, 18) + '<text class="rt" x="40" y="19">' + esc(title) + '</text><text class="rd" x="40" y="34">' + esc(sub) + '</text>'; 217 toasts.unshift({ g: g, t0: t, y: CY + 46, x: CX + CW - 208 }); 218 } 219 /* each arriving threat queues one notification; they go out as a calm pulse, one every ~1.6 s, 220 rotating threat detected â custom alert â contained, so a block reads as one short sequence */ 221 const queue = []; 222 let kindN = 0, nextOut = 0; 223 R.arrive = function (id, th) { 224 const i = THREATS.indexOf(th), k = kindN++ % 3; 225 if (queue.length < 3) queue.push(k === 0 ? ['Threat detected', th.n + ' · ' + TX[i].t, 'crit', 'search', th, i] : k === 1 ? ['Custom alert rule', th.n + ' · alert sent to Slack', 'warn', 'doc', th, i] : [respFor(i), th.n + ' · response applied', 'ok', 'shield', th, i]); 226 }; 227 R.step = function (t, dt) { 228 if (queue.length && t >= nextOut) { 229 const n = queue.shift(); 230 toast(n[0], n[1], n[2], n[3], t); 231 if (n[2] === 'ok' || n[2] === 'crit') { addRow(n[4].n + ' stopped', 'story ' + TX[n[5]].t + ' · ' + TX[n[5]].act, 'crit', t); } 232 if (n[2] === 'crit') { threats++; if (R.kpi) R.kpi.textContent = threats; } 233 nextOut = t + 1600; 234 } 235 toasts.forEach(function (o, j) { 236 const age = t - o.t0, ty = CY + 46 + j * 54, tx = CX + CW - 238; 237 o.x += (tx - o.x) * Math.min(1, dt / 120); o.y += (ty - o.y) * Math.min(1, dt / 160); 238 o.g.setAttribute('transform', 'translate(' + o.x.toFixed(1) + ' ' + o.y.toFixed(1) + ')'); 239 o.g.style.opacity = Math.max(0, Math.min(1, age / 450, (3400 - age) / 800, j < 3 ? 1 : 0)).toFixed(2); 240 o.g.classList.toggle('fresh', age < 900); 241 }); 242 for (let j = toasts.length - 1; j >= 0; j--) if (t - toasts[j].t0 > 3400 || j > 5) { toasts[j].g.remove(); toasts.splice(j, 1); } 243 rows.forEach(function (o, j) { 244 const ty = CY + 300 + j * 48; 245 o.y += (ty - o.y) * Math.min(1, dt / 200); 246 o.g.setAttribute('transform', 'translate(' + (CX + 74) + ' ' + o.y.toFixed(1) + ')'); 247 o.g.style.opacity = j < 3 ? Math.max(0, Math.min(1, (t - o.t0) / 500)).toFixed(2) : '0'; 248 });
249 if (R.bars) R.bars.forEach(function (b, j) { const base = 20 + ((j * 37) % 55), h = base + Math.sin(t * .0007 + j) * 5; b.setAttribute('height', h.toFixed(1)); b.setAttribute('y', (CY + 240 - h).toFixed(1)); }); 250 }; 251 return R; 252 } 253 const RIGHT = right === 'caps' ? null : buildRight(right), TG = RIGHT ? RIGHT.targets : CAPS; 254 255 /* capabilities on the right; swaying fibres leave the inside of the membrane and fan out to them */ 256 const offs = [-7, 0, 7]; 257 if (RIGHT) s += TG.map(function (c, i) { return '<g class="fb" data-c="' + c.id + '">' + c.F.map(function (x, k) { return '<path data-l="c' + i + '-' + k + '"/>'; }).join('') + '</g>'; }).join('') + RIGHT.svg; 258 else CAPS.forEach(function (c, i) { 259 const y = capTop + i * capGap, ys = OY + (y - OY) * .3, xs = ax(ys, INNER) + 6; 260 c.xs = xs; c.ys = ys; 261 c.F = offs.map(function (o) { return line([xs, ys + o], [xs + 110, ys + o * 1.8], [capX - 130, y + o * .6], [capX - capR - 5, y], 5 + Math.random() * 7); }); 262 s += '<g class="fb" data-c="' + c.id + '">' + c.F.map(function (f, k) { return '<path data-l="c' + i + '-' + k + '"/>'; }).join('') + '</g>' + 263 '<g class="cp" data-c="' + c.id + '">' + badge(capX, y, capR, c.ic) + '<text class="ttl" x="' + (capX + capTx) + '" y="' + (y - 2) + '">' + esc(c.n) + '</text><text class="tsb" x="' + (capX + capTx) + '" y="' + (y + 13) + '">' + c.s + '</text></g>'; 264 }); 265 s += '<g class="nt"><line x1="836" y1="612" x2="866" y2="612"/><text x="878" y="608">ATTACKS BLOCKED.</text><text x="878" y="622">ENDPOINTS PROTECTED.</text></g><g class="amb"></g><g class="fx"></g></svg>'; 266 host.innerHTML = s; 267 if (RIGHT) RIGHT.init(); 268 const svg = host.querySelector('svg'); 269 270 /* the logo, bent along the membrane: every glyph of the real mark (cross, T-h-r-e-a-t, R-E-S-P-O-N-D-E-R) 271 is placed on the arc at its own position and turned to the arc's tangent, so it reads top to bottom */ 272 const glyphs = []; 273 (function bendLogo() { 274 const wrap = document.createElement('div'); 275 wrap.innerHTML = '<span data-logo="threatresponder"></span>'; 276 if (window.Logos) window.Logos.paint(wrap); 277 const src = wrap.querySelector('svg'); 278 if (!src) return; 279 const tmp = document.createElementNS(NS, 'g'); 280 tmp.style.visibility = 'hidden'; 281 Array.prototype.forEach.call(src.querySelectorAll('path, rect, polygon'), function (n) { tmp.appendChild(document.importNode(n, true)); }); 282 svg.appendChild(tmp); 283 const W = 363.409, CY = 13.75, out = host.querySelector('.mlogo'); 284 Array.prototype.slice.call(tmp.childNodes).forEach(function (n) { 285 const b = n.getBBox(), gx = b.x + b.width / 2, th = (gx - W / 2) * LOGO_K / LOGO_R; 286 const tx = Math.sin(th), ty = Math.cos(th), nx = -Math.cos(th), ny = Math.sin(th), px = OX - LOGO_R * Math.cos(th), py = OY + LOGO_R * Math.sin(th); 287 const a = LOGO_K * tx, bb = LOGO_K * ty, c = LOGO_K * nx, d = LOGO_K * ny; 288 n.setAttribute('transform', 'matrix(' + [a, bb, c, d, px - a * gx - c * CY, py - bb * gx - d * CY].map(function (v) { return v.toFixed(4); }).join(' ') + ')'); 289 out.appendChild(n); 290 glyphs.push({ el: n, y: py }); 291 }); 292 tmp.remove(); 293 })(); 294 295 const fx = host.querySelector('.fx'), amb = host.querySelector('.amb'), thEl = host.querySelectorAll('.th'), logoG = host.querySelector('.mlogo'); 296 const capEl = {}, fbEl = {}; 297 TG.forEach(function (c, i) { capEl[c.id] = host.querySelector('[data-c="' + c.id + '"]:not(.fb)'); fbEl[c.id] = host.querySelector('.fb[data-c="' + c.id + '"]'); c.F.forEach(function (f, k) { f.el = host.querySelector('[data-l="c' + i + '-' + k + '"]'); }); });
298 if (!strike) THREATS.forEach(function (t, i) { t.L.el = host.querySelector('[data-l="t' + i + '"]'); }); 299 const ALL = [].concat.apply(strike ? [] : THREATS.map(function (t) { return t.L; }), TG.map(function (c) { return c.F; })); 300 function mk(tag, a, parent) { const e = document.createElementNS(NS, tag); Object.keys(a).forEach(function (k) { e.setAttribute(k, a[k]); }); (parent || fx).appendChild(e); return e; } 301 function ease(k) { return k < .5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2; } 302 303 /* ambient flow: particles always travelling in along the threat lines and out along the fibres */ 304 const drift = [], specks = []; 305 if (!strike) THREATS.forEach(function (t) { for (let j = 0; j < 4; j++) drift.push({ L: t.L, u0: Math.random(), dur: 2600 + Math.random() * 2800, jit: 1 + Math.random() * 3, op: .25 + Math.random() * .3, el: mk('circle', { class: 'amb-t', r: (.8 + Math.random() * 1).toFixed(2) }, amb) }); }); 306 if (!(RIGHT && RIGHT.noPulse)) TG.forEach(function (c) { for (let j = 0; j < 3; j++) drift.push({ L: c.F[Math.floor(Math.random() * c.F.length)], u0: Math.random(), dur: 2200 + Math.random() * 2600, jit: 1 + Math.random() * 2, op: .2 + Math.random() * .3, el: mk('circle', { class: 'amb-c', r: (.7 + Math.random() * .9).toFixed(2) }, amb) }); }); 307 const sg = host.querySelector('.specks'); 308 for (let i = 0; i < 54; i++) specks.push({ a: (Math.random() - .5) * 1.5, r: 380 + Math.random() * 120, sp: (Math.random() - .5) * .00005, tw: .001 + Math.random() * .003, ph: Math.random() * 6, el: mk('circle', { class: 'speck', r: (.5 + Math.random() * 1.2).toFixed(2) }, sg) }); 309 310 311 /* Waves: no standing threat list, the membrane stands alone. A faint drift of particles always flows toward 312 it, and every few seconds a block of related threats (a campaign) flies in together and strikes it */ 313 const BLOCKS = [ 314 { n: 'PHISHING CAMPAIGN', t: [0, 2, 4] }, 315 { n: 'RANSOMWARE OPERATION', t: [3, 5, 1] }, 316 { n: 'COMMAND & CONTROL', t: [6, 2] }, 317 { n: 'INTRUSION', t: [3, 4, 5] }, 318 { n: 'CREDENTIAL ATTACK', t: [0, 4] } 319 ]; 320 const waves = [], loose = []; 321 let nextWave = 300, bi = Math.floor(Math.random() * BLOCKS.length); 322 function resetLoose(p, fresh) { p.x = fresh ? Math.random() * 420 : -10 - Math.random() * 40; p.y = 70 + Math.random() * 520; p.vx = 30 + Math.random() * 55; p.op = .15 + Math.random() * .3; } 323 if (strike) for (let i = 0; i < 26; i++) { const p = { el: mk('circle', { class: 'amb-t', r: (.7 + Math.random() * 1).toFixed(2) }, amb) }; resetLoose(p, true); loose.push(p); } 324 function chip(th) { 325 const g = mk('g', { class: 'chip th on' }); 326 g.innerHTML = '<circle class="ring" r="19"/>' + icon(th.ic, -9, -9, 18) + '<text class="ttl" x="-30" y="-2" text-anchor="end">' + esc(th.n) + '</text><text class="tsb" x="-30" y="12" text-anchor="end">' + th.s + '</text>'; 327 return g; 328 } 329 function strikeStep(t, dt, P, hits, onC, onF) { 330 loose.forEach(function (p) { 331 p.x += p.vx * dt / 1000; p.y += (OY - p.y) * .00012 * dt; 332 const edge = ax(p.y, RIM) - 4; 333 if (p.x > edge) resetLoose(p, false); 334 p.el.setAttribute('cx', p.x.toFixed(1)); p.el.setAttribute('cy', p.y.toFixed(1)); 335 p.el.style.opacity = (p.op * Math.min(1, Math.max(0, p.x + 10) / 80, Math.max(0, edge - p.x) / 60)).toFixed(2); 336 }); 337 if (t >= nextWave) { 338 const B = BLOCKS[bi++ % BLOCKS.length], n = B.t.length, cy = 210 + Math.random() * 220; 339 const w = { t0: t, label: mk('text', { class: 'wlbl' }), items: [] }; 340 w.label.textContent = B.n; 341 B.t.forEach(function (ti, j) { 342 const th = THREATS[ti], y0 = cy + (j - (n - 1) / 2) * 72 + (Math.random() - .5) * 12, iy = OY + (y0 - OY) * .8; 343 w.items.push({ th: th, x0: 70 + (Math.random() - .5) * 40 + (j % 2) * 34, y0: y0, ix: ax(iy, RIM + 2) - 2, iy: iy, delay: j * 170 + Math.random() * 150, dur: 2600 + Math.random() * 600, 344 sway: (Math.random() - .5) * 14, el: chip(th), trail: [1.8, 1.3, .9].map(function (r) { return mk('circle', { class: 'pa2', r: r }); }), flare: mk('circle', { class: 'flare', r: 0 }), 345 pb: (RIGHT ? RIGHT.pick(th) : th.caps).map(function () { return { d: mk('circle', { class: 'pb', r: 2.6 }), f: Math.floor(Math.random() * 3) }; }) }); 346 }); 347 waves.push(w); 348 nextWave = t + 4400 + Math.random() * 2200; 349 } 350 for (let wi = waves.length - 1; wi >= 0; wi--) { 351 const w = waves[wi]; 352 let done = true, lx = 1e9, ly = 1e9, lead = 0;
353 w.items.forEach(function (it) { 354 const k = t - w.t0 - it.delay, D = it.dur, q = Math.max(0, Math.min(1, k / D)); 355 if (k < D + 3200) done = false; 356 const pos = function (u) { const ee = .45 * u + .55 * u * u, s2 = u * u * (3 - 2 * u); return [it.x0 + (it.ix - it.x0) * ee, it.y0 + (it.iy - it.y0) * s2 + Math.sin(u * Math.PI) * it.sway]; }; 357 if (k >= 0 && k < D) { 358 const p = pos(q), sc = q > .86 ? 1 - (q - .86) / .14 * .45 : 1; 359 it.el.setAttribute('transform', 'translate(' + p[0].toFixed(1) + ' ' + p[1].toFixed(1) + ') scale(' + sc.toFixed(3) + ')'); 360 it.el.style.opacity = Math.min(1, k / 450, (1 - q) * 8 + .2).toFixed(2); 361 it.trail.forEach(function (d, j) { const r = pos(Math.max(0, q - (j + 1) * .03)); d.setAttribute('cx', r[0] - 22); d.setAttribute('cy', r[1]); d.style.opacity = String((.45 - j * .12) * Math.min(1, k / 450)); }); 362 lx = Math.min(lx, p[0]); ly = Math.min(ly, p[1]); lead = Math.max(lead, q); 363 } else { 364 it.el.style.opacity = '0'; it.trail.forEach(function (d) { d.style.opacity = '0'; }); 365 } 366 const kf = (k - D) / 1100; 367 if (kf >= 0 && kf <= 1) { it.flare.setAttribute('cx', it.ix); it.flare.setAttribute('cy', it.iy); it.flare.setAttribute('r', (6 + 36 * kf).toFixed(1)); it.flare.style.opacity = (1 - kf).toFixed(2); } else it.flare.style.opacity = '0'; 368 if (k >= D && k < D + 1400) hits.push({ y: it.iy, p: Math.exp(-(k - D) / 420) }); 369 /* Layers takes no pulses: the hit itself is handed over, and the layers light up on their own pace */ 370 if (RIGHT && RIGHT.noPulse && k >= D + 200 && !it.arrived) { it.arrived = true; RIGHT.arrive(null, it.th, t); } 371 (RIGHT ? RIGHT.pick(it.th) : it.th.caps).forEach(function (cid, j) { 372 const c = TG.find(function (x) { return x.id === cid; }), kb = (k - D - 260 - j * (RIGHT ? 380 : 140)) / 900, b = it.pb[j]; 373 if (RIGHT && kb >= 1 && !b.arr) { b.arr = true; RIGHT.arrive(cid, it.th, t); } 374 if (kb >= 0 && kb <= 1) { const p = bez(P.get(c.F[b.f]), ease(kb)); b.d.setAttribute('cx', p[0]); b.d.setAttribute('cy', p[1]); b.d.style.opacity = ''; onF[cid] = true; } else b.d.style.opacity = '0'; 375 if (kb > .85 && kb < 3) onC[cid] = true; 376 }); 377 }); 378 /* the campaign's name rides above the block and fades as it closes in */ 379 if (lx < 1e9) { w.label.setAttribute('x', (lx - 150).toFixed(1)); w.label.setAttribute('y', (ly - 36).toFixed(1)); w.label.style.opacity = Math.max(0, Math.min(1, (t - w.t0) / 500, (.7 - lead) * 3)).toFixed(2); } else w.label.style.opacity = '0'; 380 if (done) { w.label.remove(); w.items.forEach(function (it) { [it.el, it.flare].concat(it.trail, it.pb.map(function (b) { return b.d; })).forEach(function (n) { n.remove(); }); }); waves.splice(wi, 1); } 381 } 382 } 383 384 const events = []; 385 let next = 200, last = -1, prevT = 0; 386 upd = function (t) { 387 const dt = Math.max(0, Math.min(50, t - prevT)); 388 prevT = t; 389 const P = new Map(); 390 ALL.forEach(function (L) { const c = cur(L, t); P.set(L, c); L.el.setAttribute('d', dOf(c)); }); 391 drift.forEach(function (d) { 392 const u = ((t / d.dur) + d.u0) % 1, p = bez(P.get(d.L), u), w = Math.sin(t * .002 + d.u0 * 20) * d.jit; 393 d.el.setAttribute('cx', p[0].toFixed(1)); d.el.setAttribute('cy', (p[1] + w).toFixed(1)); 394 d.el.style.opacity = (d.op * Math.min(1, u * 6, (1 - u) * 6)).toFixed(2); 395 }); 396 specks.forEach(function (k) { 397 k.a += k.sp * dt; if (k.a > .78) k.a = -.78; if (k.a < -.78) k.a = .78; 398 k.el.setAttribute('cx', (OX - k.r * Math.cos(k.a)).toFixed(1)); k.el.setAttribute('cy', (OY + k.r * Math.sin(k.a)).toFixed(1)); 399 k.el.style.opacity = (.12 + .4 * Math.abs(Math.sin(t * k.tw + k.ph))).toFixed(2); 400 }); 401 const onT = {}, onC = {}, onF = {}, hits = []; 402 if (RIGHT) RIGHT.step(t, dt); 403 if (strike) strikeStep(t, dt, P, hits, onC, onF); 404 else { 405 if (t >= next) { 406 let i = Math.floor(Math.random() * THREATS.length); 407 if (i === last) i = (i + 1 + Math.floor(Math.random() * (THREATS.length - 1))) % THREATS.length; 408 last = i; 409 const th = THREATS[i];
410 events.push({ i: i, t0: t, pa: mk('circle', { class: 'pa', r: 3 }), trail: [mk('circle', { class: 'pa2', r: 2 }), mk('circle', { class: 'pa2', r: 1.4 })], 411 flare: mk('circle', { class: 'flare', cx: th.ix, cy: th.iy, r: 0 }), dur: 1000 + Math.random() * 500, 412 pb: th.caps.map(function () { return { d: mk('circle', { class: 'pb', r: 2.6 }), f: Math.floor(Math.random() * offs.length) }; }) }); 413 next = t + 1300 + Math.random() * 1300; 414 } 415 for (let e = events.length - 1; e >= 0; e--) { 416 const ev = events[e], th = THREATS[ev.i], k = t - ev.t0, D = ev.dur; 417 if (k < 0) continue; 418 if (k > D + 3000) { [ev.pa, ev.flare].concat(ev.trail, ev.pb.map(function (b) { return b.d; })).forEach(function (n) { n.remove(); }); events.splice(e, 1); continue; } 419 if (k < D + 600) onT[ev.i] = true; 420 if (k < D) { 421 const q = ease(k / D), p = bez(P.get(th.L), q); 422 ev.pa.setAttribute('cx', p[0]); ev.pa.setAttribute('cy', p[1]); ev.pa.style.opacity = ''; 423 ev.trail.forEach(function (d, j) { const r = bez(P.get(th.L), Math.max(0, q - (j + 1) * .035)); d.setAttribute('cx', r[0]); d.setAttribute('cy', r[1]); d.style.opacity = ''; }); 424 } else { ev.pa.style.opacity = '0'; ev.trail.forEach(function (d) { d.style.opacity = '0'; }); } 425 const kf = (k - D) / 1100; 426 if (kf >= 0 && kf <= 1) { ev.flare.setAttribute('r', (6 + 34 * kf).toFixed(1)); ev.flare.style.opacity = (1 - kf).toFixed(2); } else ev.flare.style.opacity = '0'; 427 /* the logo pulses around where the threat hit, strongest there and fading along the curve */ 428 if (k >= D && k < D + 1400) hits.push({ y: th.iy, p: Math.exp(-(k - D) / 420) }); 429 th.caps.forEach(function (cid, j) { 430 const c = CAPS.find(function (x) { return x.id === cid; }), kb = (k - D - 260 - j * 140) / 900, b = ev.pb[j]; 431 if (kb >= 0 && kb <= 1) { const p = bez(P.get(c.F[b.f]), ease(kb)); b.d.setAttribute('cx', p[0]); b.d.setAttribute('cy', p[1]); b.d.style.opacity = ''; onF[cid] = true; } else b.d.style.opacity = '0'; 432 if (kb > .85 && kb < 3) onC[cid] = true; 433 }); 434 } 435 } 436 let peak = 0; 437 glyphs.forEach(function (g) { 438 let I = 0; 439 hits.forEach(function (h) { I += h.p * Math.exp(-Math.pow((g.y - h.y) / 120, 2)); }); 440 I = Math.min(1, I); peak = Math.max(peak, I); 441 g.el.style.opacity = (.42 + .58 * I).toFixed(3); 442 }); 443 logoG.style.filter = peak > .02 ? 'drop-shadow(0 0 ' + (3 + 9 * peak).toFixed(1) + 'px rgba(170, 200, 255, ' + (.15 + .55 * peak).toFixed(2) + '))' : ''; 444 thEl.forEach(function (el, i) { el.classList.toggle('on', !!onT[i]); }); 445 if (!(RIGHT && RIGHT.noPulse)) TG.forEach(function (c) { capEl[c.id].classList.toggle('on', !!onC[c.id]); fbEl[c.id].classList.toggle('on', !!onF[c.id] || !!onC[c.id]); }); 446 }; 447 448 function layout() { 449 const hb = hero.getBoundingClientRect(), tb = text.getBoundingClientRect(), sb = (function () { const band = hero.querySelector('.tb'); return band && band.offsetParent ? band : shot; })().getBoundingClientRect(); 450 const left = tb.right - hb.left + 24; 451 host.style.left = left + 'px'; host.style.top = '16px'; 452 host.style.width = Math.max(200, hero.clientWidth - left - 12) + 'px'; host.style.height = Math.max(200, sb.top - hb.top - 24) + 'px'; 453 } 454 layout(); 455 document.addEventListener('site:ready', layout); 456 if (document.fonts) document.fonts.ready.then(layout); 457 let rz = 0; 458 window.addEventListener('resize', function () { clearTimeout(rz); rz = setTimeout(layout, 120); }); 459 460 let running = true, t0 = 0, lastNow = 0; 461 window.__story = { frozen: false, seek: function (t) { window.__story.frozen = true; running = false; for (let k = Math.max(0, t - 4000); k <= t; k += 16) upd(k); } }; 462 if (still) { for (let k = 0; k <= 1900; k += 30) upd(k); return; } 463 (function loop(now) { 464 if (running) { if (!t0) t0 = now; upd(now - t0); } else if (lastNow) t0 += now - lastNow; 465 lastNow = now; 466 requestAnimationFrame(loop); 467 })(0); 468 new IntersectionObserver(function (es) { if (window.__story.frozen) return; running = es[0].isIntersecting; }).observe(hero); 469 })(); 470 471 /* Globe hero (?hero=globe): the supplied globe artwork as the backdrop, with the product composed over it 472 after the user's mock: Detection, Response, Investigation and Evidence cards wired to a shield at the 473 centre. Everything sits on a 1916Ã821 stage in the artwork's own pixels (globe centre 1235,545, 474 radius 443), scaled with it, so it stays locked to the globe at any width. The cards are still (their 475 content is a placeholder); the only motion is the connectors flowing into the shield. */ 476 (function () { 477 if (window.__heroMode !== 'globe') return; 478 const hero = document.getElementById('hero'); 479 if (!hero) return; 480 const ic = function (n) { return window.PUI ? window.PUI.icon(n) : ''; };
481 const bars = '<svg class="pu-i" viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="12" width="3.2" height="8" rx="1"/><rect x="10.4" y="6" width="3.2" height="14" rx="1"/><rect x="16.8" y="9" width="3.2" height="11" rx="1"/></svg>'; 482 const card = function (cls, box, icon, title, body) { 483 return '<div class="gc ' + cls + '" style="left:' + box[0] + 'px;top:' + box[1] + 'px;width:' + box[2] + 'px;height:' + box[3] + 'px"><h5>' + icon + '<span>' + title + '</span></h5>' + body + '</div>'; 484 }; 485 const RESP = [['lock', 'Contain endpoint'], ['bolt', 'Kill process'], ['archive', 'Quarantine'], ['clock', 'Revert changes'], ['search', 'Hunt']]; 486 const INV = [['file', 'Files'], ['cog', 'Processes'], ['net', 'Network'], ['reg', 'Registry'], ['user', 'Users'], ['clock', 'Timeline']]; 487 const EVI = [['10:24', 'Malicious file blocked', 'r'], ['10:25', 'Process terminated', ''], ['10:27', 'Artifacts collected', ''], ['10:28', 'Incident resolved', 'g']]; 488 489 const glb = document.createElement('div'); 490 glb.className = 'glb'; glb.setAttribute('aria-hidden', 'true'); 491 glb.innerHTML = '<img class="glb-bg dk" src="/shared/hero-globe.webp" alt=""><img class="glb-bg lt" src="/shared/hero-globe-light.webp" alt="">' + 492 '<div class="glb-st">' + 493 '<svg class="glb-svg" viewBox="0 0 1916 821" width="1916" height="821">' + 494 '<defs><radialGradient id="gcore" cx="50%" cy="45%" r="60%"><stop offset="0" stop-color="#1d4fb8"/><stop offset="1" stop-color="#0a1f5e"/></radialGradient>' + 495 '<linearGradient id="gshield" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#bfe0ff"/><stop offset="1" stop-color="#3f8cff"/></linearGradient>' + 496 '<filter id="gglow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>' + 497 '<g class="gl-links" filter="url(#gglow)">' + 498 ['M1163,413 V433 Q1163,447 1177,447 H1232 Q1242,447 1250,455', 499 'M1355,365 H1345 Q1331,365 1331,379 V441', 500 'M1213,517 H1236 Q1248,517 1253,507', 501 'M1339,508 Q1345,517 1357,517 H1419 Q1433,517 1433,531 V532'].map(function (d) { return '<path class="lk" d="' + d + '"/><path class="lk-f" d="' + d + '"/>'; }).join('') + '</g>' + 502 '<g class="gl-core" transform="translate(1295 480)"><circle class="gr-o" r="52"/><circle class="gr-i" r="40" fill="url(#gcore)"/>' + 503 '<path class="gr-s" d="M0,-24 L19,-16 V0 C19,13 10,21 0,26 C-10,21 -19,13 -19,0 V-16 Z" fill="url(#gshield)"/><path class="gr-h" d="M0,-24 V26 C-10,21 -19,13 -19,0 V-16 Z"/></g>' + 504 '</svg>' + 505 card('det', [1060, 253, 235, 160], bars, 'Detection', '<div class="gd"><div><b class="gn">1,248</b><small>Threats blocked</small></div><div class="gt"><em>â¼ 87%</em><small>vs. last 30 days</small></div></div>') + 506 card('rsp', [1355, 263, 193, 213], ic('cog'), 'Response', '<ul>' + RESP.map(function (r) { return '<li>' + ic(r[0]) + r[1] + '</li>'; }).join('') + '</ul>') + 507 card('inv', [999, 472, 214, 249], ic('search'), 'Investigation', '<ul>' + INV.map(function (r) { return '<li>' + ic(r[0]) + '<span>' + r[1] + '</span><i class="cv"></i></li>'; }).join('') + '</ul>') + 508 card('evi', [1345, 532, 250, 181], ic('file'), 'Evidence', '<ol>' + EVI.map(function (r) { return '<li class="' + r[2] + '"><i></i><time>' + r[0] + '</time>' + r[1] + '</li>'; }).join('') + '</ol>') + 509 '<div class="gl-tag">DETECT <i>·</i> PREVENT <i>·</i> <b>INVESTIGATE</b> <i>·</i> RESPOND</div>' + 510 '</div>'; 511 hero.insertBefore(glb, hero.firstChild); 512 const st = glb.querySelector('.glb-st'); 513 514 /* the cards must clear the text: measure where the words actually end, then scale the globe down (to 515 72% at most, kept centred in the band) until the Investigation card (stage x 999) clears it; only 516 what is still missing after that shifts the globe right */ 517 const textCol = hero.querySelector('.hgrid > div'); 518 function textRight() { 519 let r = 0; 520 const w = document.createTreeWalker(textCol, NodeFilter.SHOW_TEXT), rg = document.createRange(); 521 while (w.nextNode()) { 522 if (!w.currentNode.textContent.trim()) continue; 523 rg.selectNodeContents(w.currentNode); 524 rg.getClientRects && Array.prototype.forEach.call(rg.getClientRects(), function (b) { r = Math.max(r, b.right); }); 525 } 526 textCol.querySelectorAll('img, .btn').forEach(function (e) { r = Math.max(r, e.getBoundingClientRect().right); }); 527 return r; 528 } 529 function fit() { 530 glb.style.height = glb.style.top = glb.style.right = ''; 531 if (window.innerWidth <= 1100) { st.style.transform = ''; return; } 532 const band = glb.clientHeight, hb = hero.getBoundingClientRect(), vw = hero.clientWidth; 533 const need = textRight() - hb.left + 28, left = function (sc) { return vw - (1916 - 999) * sc; }; 534 let s = band / 821; 535 if (left(s) < need) s = Math.max(band / 821 * .72, (vw - need) / (1916 - 999)); 536 const shift = Math.max(0, need - left(s)), h = 821 * s; 537 glb.classList.toggle('sc', h < band - 1); 538 glb.style.height = h.toFixed(1) + 'px'; glb.style.top = ((band - h) / 2).toFixed(1) + 'px'; glb.style.right = (-shift).toFixed(1) + 'px'; 539 st.style.transform = 'scale(' + s.toFixed(4) + ')'; 540 } 541 fit(); 542 window.addEventListener('resize', fit); 543 document.addEventListener('site:ready', fit); 544 if (document.fonts) document.fonts.ready.then(fit); 545 546 })(); 547 548 /* After the hero (?next=pillars|pipeline|layers|bento|screen): the live site's "Predict, Stop, & 549 Investigate Attacks" section, with its exact heading and body, and the platform drawn four ways. Nothing 550 pins: the section scrolls like any other. 551 pillars (after the user's picture 1): Mission Control wired down to five capability cards standing 552 on the red lightweight-agent slab; assembles once in view, then data rises up the wires 553 pipeline (after the user's picture 2): endpoints, identities and activity â the agent â five 554 integrated capabilities â Mission Control with a real panel; Collect / Detect / Investigate / 555 Respond take turns, lighting what does the work 556 layers (static): console / capabilities / agent as three labelled bands 557 bento (static): the slogan as columns, a small product-style picture in each big tile
558 The section takes the opposite tone of the hero. screen keeps the product screenshot instead. */ 559 (function () { 560 const root = document.documentElement, mode = root.getAttribute('data-next'); 561 const hero = document.getElementById('hero'); 562 if (!hero || !mode || mode === 'screen') return; 563 const still = root.classList.contains('poster') || root.classList.contains('reduced'); 564 const I = function (n) { return '<i data-ic="' + n + '"></i>'; }; 565 566 /* the capabilities, each named once, with what it actually does */ 567 const CAP = [ 568 { k: 'edr', ic: 'shield', t: 'Detection & response', d: 'Stops the attack on the endpoint and tells it as one story.', pui: 'triage', url: 'threats', 569 f: ['Ransomware, exploit and script blocking', 'Threat stories mapped to MITRE ATT&CK', 'Kill, quarantine, contain, revert', 'Custom detection rules'] }, 570 { k: 'itdr', ic: 'user', t: 'Identity threat protection', d: 'Sees the accounts an attacker would use, from the same agent.', pui: 'terrain', url: 'identity', 571 f: ['Risky and exposed accounts', 'Identity-based attack detection', 'Lateral movement and paths to Tier 0'] }, 572 { k: 'exp', ic: 'bug', t: 'Exposure', d: 'Finds what an attacker could use, before they do.', pui: 'exposure', url: 'exposure', 573 f: ['CVEs in real time, without scanning', 'Known-exploited (CISA KEV) first', 'BOD 22-01 prioritised', 'Security posture score'] }, 574 { k: 'for', ic: 'search', t: 'Forensics & IR', d: 'Investigates remotely, even after a breach.', pui: 'forensics', url: 'forensics', 575 f: ['Remote forensic collection', 'Super-timeline and threat hunting', 'Live response on any endpoint', 'Offline collection for isolated hosts'] }]; 576 const CONSOLE = ['Multi-tenant, built for MSSPs', 'Threat stories and triage', 'Dashboards across every company', 'SIEM integration']; 577 const AGENT = ['Windows', 'macOS', 'Linux', 'Deploys in minutes', 'Works after a breach', 'Offline evidence collection']; 578 const logo = '<img class="plx-logo" src="/shared/threatresponder-logo.svg" alt="ThreatResponder®">'; 579 const list = function (a) { return '<ul class="plx-f">' + a.map(function (x) { return '<li>' + I('check') + '<span>' + x + '</span></li>'; }).join('') + '</ul>'; }; 580 581 /* two-tone icons for the two picture variants: fill is the icon colour, --i2 the accent */ 582 const SV = function (b) { return '<svg viewBox="0 0 24 24" aria-hidden="true">' + b + '</svg>'; }; 583 const G = { 584 edr: SV('<path d="M12 2l8 3v6c0 5-3.5 9-8 11-4.5-2-8-6-8-11V5z" fill="currentColor"/><circle cx="12" cy="11.5" r="3.6" fill="none" stroke="#fff" stroke-width="1.6"/><path d="M12 6.6v2.6M12 13.8v2.6M7.1 11.5h2.6M14.3 11.5h2.6" stroke="#fff" stroke-width="1.6" stroke-linecap="round"/>'), 585 itdr: SV('<circle cx="10" cy="7" r="4" fill="currentColor"/><path d="M2 21.5c0-4.6 3.6-8 8-8 1.4 0 2.7.3 3.8.9V21.5z" fill="currentColor"/><path d="M18.5 11.5l4 1.5v3c0 2.7-1.7 4.7-4 5.6-2.3-.9-4-2.9-4-5.6v-3z" fill="var(--i2)"/>'), 586 threat: SV('<circle cx="10.5" cy="10.5" r="7.2" fill="none" stroke="currentColor" stroke-width="2.2"/><path d="M15.8 15.8l5.4 5.4" stroke="currentColor" stroke-width="2.8" stroke-linecap="round"/><path d="M10.5 6.4l4 7h-8z" fill="var(--i2)"/><path d="M10.5 9.2v2.2" stroke="#fff" stroke-width="1.2" stroke-linecap="round"/>'), 587 ir: SV('<path d="M4.5 2h9l5 5v8.5a6 6 0 0 0-4 6.5H4.5z" fill="currentColor"/><path d="M7.5 9h7M7.5 12.5h5" stroke="#fff" stroke-width="1.6" stroke-linecap="round"/><circle cx="18" cy="18" r="4.2" fill="var(--i2)"/><circle cx="18" cy="18" r="1.5" fill="#fff"/>'), 588 exp: SV('<rect x="3" y="14" width="4" height="7.5" rx="1" fill="currentColor"/><rect x="10" y="10.5" width="4" height="11" rx="1" fill="currentColor"/><rect x="17" y="7.5" width="4" height="14" rx="1" fill="currentColor"/><path d="M3 10.5l6-4 4 2 7-6.2" fill="none" stroke="var(--i2)" stroke-width="2" stroke-linecap="round"/><path d="M16.5 2.2h3.8V6" fill="none" stroke="var(--i2)" stroke-width="2" stroke-linecap="round"/>'), 589 monitor: SV('<rect x="2" y="3" width="20" height="14" rx="2.2" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M8.5 21h7M12 17v4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
589<path d="M7.5 13.5v-3M11 13.5V7.5M14.5 13.5v-4.5M18 13.5V9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>'), 590 bolt: SV('<path d="M12 2l8 3v6c0 5-3.5 9-8 11-4.5-2-8-6-8-11V5z" fill="currentColor"/><path d="M13.2 5.8L8.8 13h3.1l-1.1 5.2 4.4-7.3h-3.1z" fill="var(--i2)"/>'), 591 plus: SV('<path d="M12 2l8 3v6c0 5-3.5 9-8 11-4.5-2-8-6-8-11V5z" fill="currentColor"/><path d="M12 7.2v8.6M7.7 11.5h8.6" stroke="#fff" stroke-width="2.6" stroke-linecap="round"/>'), 592 laptop: SV('<rect x="4" y="4" width="16" height="11" rx="1.6" fill="none" stroke="currentColor" stroke-width="2"/><path d="M2 18.5h20" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>'), 593 users: SV('<circle cx="8.5" cy="8" r="3.4" fill="currentColor"/><circle cx="16.5" cy="8.5" r="2.8" fill="currentColor" opacity=".7"/><path d="M2 19.5c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6z" fill="currentColor"/><path d="M15 13.8c3.4-.4 7 1.6 7 5.7h-5" fill="currentColor" opacity=".7"/>'), 594 activity: SV('<path d="M2 12.5h4l2.5-6 4.5 12 3-8h6" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>'), 595 db: SV('<ellipse cx="12" cy="5.5" rx="7.5" ry="3" fill="none" stroke="currentColor" stroke-width="2"/><path d="M4.5 5.5v13c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-13M4.5 12c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3" fill="none" stroke="currentColor" stroke-width="2"/>'), 596 search: SV('<circle cx="10.5" cy="10.5" r="6.5" fill="none" stroke="currentColor" stroke-width="2.4"/><path d="M15.5 15.5l5 5" stroke="currentColor" stroke-width="2.8" stroke-linecap="round"/>'), 597 doc: SV('<path d="M5 2h9.5l4.5 4.5V22H5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M8.5 11h7M8.5 14.5h7M8.5 18h4.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>'), 598 cog: SV('<circle cx="12" cy="12" r="3.3" fill="none" stroke="currentColor" stroke-width="2.2"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.3 5.3l2.1 2.1M16.6 16.6l2.1 2.1M5.3 18.7l2.1-2.1M16.6 7.4l2.1-2.1" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>') 599 }; 600 /* the five capabilities as both pictures name them; red and blue alternate as in the images */ 601 /* the logo's own parts, cropped from the official file: the grey square with the red plus, and the wordmark */ 602 const MARK = '<span class="tr-mark"><img src="/shared/threatresponder-logo.svg" alt=""></span>'; 603 const WORD = '<span class="tr-word"><img src="/shared/threatresponder-logo.svg" alt="ThreatResponder®"></span>'; 604 const icon = function (k) { return k === 'edr' ? MARK : G[k]; }; 605 const FIVE = [['edr', 'b', 'Endpoint Detection & Response', 'Detect and stop threats on endpoints, mapped to MITRE ATT&CK.', 'Stopped as they run, mapped to MITRE ATT&CK'], 606 ['itdr', 'r', 'Identity Threat Protection', 'Find and prevent identity-based attacks.', 'Risky accounts and paths to Tier 0'], 607 ['threat', 'b', 'Threat Hunting', 'Hunt for threats across every endpoint.', 'One query across every endpoint'], 608 ['ir', 'r', 'Incident Response & Forensics', 'Investigate, contain, and respond faster.', 'Remote and offline evidence collection'], 609 ['exp', 'b', 'Vulnerability & Exposure Detection', 'Find and prioritize risk across your environment.', 'CVEs in real time, without scanning']]; 610 611 let body = ''; 612 if (mode === 'pillars') { 613 body = '<div class="pi"><svg class="pi-wires" aria-hidden="true"></svg>' + 614 '<div class="pi-mc"><span class="pi-mi">' + G.monitor + '</span><div><b>Mission Control</b><small>Unified console</small></div></div>' + 615 '<div class="pi-cards">' + FIVE.map(function (c, i) { 616 return '<div class="pi-card" style="--i:' + i + '"><span class="pi-ic ic-' + c[1] + '">' + icon(c[0]) + '</span><i class="pi-dash"></i><b>' + c[2] + '</b><span class="pi-d">' + c[4] + '</span></div>'; 617 }).join('') + '</div>' + 618 '<div class="pi-agent"><span class="pi-ai">' + MARK + '</span><i class="pi-sep"></i><div><b>Lightweight Endpoint Agent</b><small>One agent. Many capabilities.</small></div></div>' + 619 '<div class="pi-foot pi-proof-footer"><aside class="pi-performance" aria-label="Independent performance test results">' + 620 '<div class="pi-performance-brand"><img src="/shared/av-comparatives-logo.png" width="162" height="152" alt="AV-Comparatives" loading="lazy"></div>' + 621 '<div class="pi-performance-copy"><h3>Lowest measured system impact among tested business security products â AV-Comparatives, 2024 & 2025.</h3>' + 622 '<p>Ranked <strong>#1</strong> in Business Performance in <a href="https://av-comparatives.org/tests/business-security-test-2024-august-november/#performance-test-result" target="_blank" rel="noopener">November 2024</a> (Impact Score <strong>3.8</strong>) and <strong>#1</strong> in November 2025 (Impact Score <strong>2.8</strong>).</p>' + 623 '<p>Rated âVery Fastâ across all performance subtests in both Windows-based tests.<a class="pi-test-link" href="https://av-comparatives.org/tests/business-security-test-2025-august-november/#performance-test-result" target="_blank" rel="noopener">Test results <span aria-hidden="true">â</span></a></p></div></aside>' + 624 '<ul class="pi-os-support" aria-label="ThreatResponder supported operating systems">' + 625 '<li><svg viewBox="0 0 28 28" aria-hidden="true"><path d="M4 6l8-1v8H4zm10-1 10-2v10H14zM4 15h8v8l-8-1zm10 0h10v11l-10-2z"/></svg>Windows</li>' + 626 '<li><svg viewBox="0 0 28 28" aria-hidden="true"><path d="M18 6c-2 1-4 1-5 0-3-1-6 1-7 4-2 5 1 13 5 16 2 1 3-1 5-1s3 2 5 0c2-2 4-5 4-7-5-2-6-7-2-10-1-2-3-3-5-2ZM15 5c0-3 2-5 5-5 0 3-2 5-5 5z"/></svg>macOS</li>' + 627 '<li><svg viewBox="0 0 28 28" aria-hidden="true"><path d="M14 2c-5 0-6 5-5 9-1 3-5 5-5 11 0 3 4 3 7 2 2 2 5 2 7 0 4 2 8 0 7-3-1-5-5-8-5-11 0-4-1-8-6-8z"/><ellipse class="os-cutout" cx="14" cy="17" rx="5" ry="7"/><ellipse class="os-cutout" cx="12" cy="8" rx="1.2" ry="1.8"/><ellipse class="os-cutout" cx="17" cy="8" rx="1.2" ry="1.8"/><path class="os-cutout" d="m12 11 3-2 3 2-3 2z"/></svg>Linux</li></ul></div></div>'; 628 } 629 if (mode === 'pipeline') { 630 const SRC = [['laptop', 'Endpoints', 'Laptops, desktops, servers, cloud VMs', 'Collect'], ['users', 'Users & identity', 'Accounts, logons, privileges', 'Monitor'], ['activity', 'Activity', 'Processes, files, network connections', 'Record']]; 631 const STEP = [['db', 'b', 'Collect', 'One agent gathers what every endpoint and identity does.'], ['search', 'r', 'Detect', 'Known and unknown threats, in real time.'], 632 ['doc', 'b', 'Investigate', 'The full context to understand and contain.'], ['cog', 'r', 'Respond', 'Kill, quarantine, contain, revert.']]; 633 body = '<div class="pp"><svg class="pp-wires" aria-hidden="true"></svg>
633' + 634 '<div class="pp-in">' + SRC.map(function (s) { return '<div class="pp-src"><span class="pp-sic">' + G[s[0]] + '</span><div><b>' + s[1] + '</b><span>' + s[2] + '</span></div></div>'; }).join('') + '</div>' + 635 SRC.map(function (s) { return '<span class="pp-pill">' + s[3] + '</span>'; }).join('') + 636 '<div class="pp-agent"><div class="pp-ring">' + MARK + WORD + '<b>Agent</b><small>Lightweight. Always on.</small></div></div>' + 637 '<div class="pp-caps"><small class="pp-lab">Integrated capabilities</small><div class="pp-row">' + FIVE.map(function (c) { 638 return '<div class="pp-cap t-' + c[1] + '"><span class="pp-cic">' + icon(c[0]) + '</span><b>' + c[2] + '</b><i></i><span>' + c[3] + '</span></div>'; 639 }).join('') + '</div></div>' + 640 '<div class="pp-mc"><b>Mission Control</b><small>Unified console</small><div class="pp-screen"><div data-pui="overview" data-skin="navy" data-loop></div></div><b class="pp-tag">See. Investigate. Respond.</b><small>All from one place.</small></div>' + 641 '<div class="pp-steps">' + STEP.map(function (s, i) { return '<div class="pp-step" data-i="' + i + '"><span class="pp-stic ic-' + s[1] + '">' + G[s[0]] + '</span><div><b>' + s[2] + '</b><span>' + s[3] + '</span></div></div>'; }).join('') + '</div></div>'; 642 } 643 if (mode === 'layers') { 644 body = '<div class="la">' + 645 '<div class="la-band la-console"><div class="la-lab"><b>Console</b><span>Mission Control</span></div><div class="la-body"><div class="la-mc"><b>Mission Control</b><span>One console for every endpoint and every company.</span></div>' + 646 '<div class="la-chips">' + CONSOLE.map(function (x) { return '<span>' + x + '</span>'; }).join('') + '</div></div></div>' + 647 '<div class="la-band la-caps"><div class="la-lab"><b>Capabilities</b><span>on the same agent</span></div><div class="la-body la-grid">' + CAP.map(function (c) { 648 return '<div class="la-cap"><div class="la-h">' + I(c.ic) + '<b>' + c.t + '</b></div><p>' + c.d + '</p>' + list(c.f) + '</div>'; 649 }).join('') + '</div></div>' + 650 '<div class="la-band la-agent"><div class="la-lab"><b>Agent</b><span>one per endpoint</span></div><div class="la-body"><div class="la-ag">' + logo + '<b>One lightweight agent</b></div>' + 651 '<div class="la-chips">' + AGENT.map(function (x) { return '<span>' + x + '</span>'; }).join('') + '</div></div></div></div>'; 652 } 653 if (mode === 'bento') { 654 const cve = '<div class="bx-cve">' + [['CVE-2024-3400', 'PAN-OS', '10.0'], ['CVE-2023-4966', 'Citrix NetScaler', '9.4'], ['CVE-2024-21412', 'Windows SmartScreen', '8.1']].map(function (c) { 655 return '<div><code>' + c[0] + '</code><span>' + c[1] + '</span><em>KEV</em><b>' + c[2] + '</b></div>'; }).join('') + '</div>'; 656 const tree = '<div class="bx-tree"><div><code>outlook.exe</code></div><div class="d1"><code>invoice.js</code></div><div class="d2"><code>powershell.exe -enc â¦</code><em>KILLED</em></div><div class="d2"><code>stage.dll</code><em class="q">QUARANTINED</em></div></div>'; 657 const chain = '<div class="bx-chain">' + [['T1566', 'Phishing'], ['T1059', 'Script'], ['T1053', 'Scheduled task'], ['T1003', 'Credential dump']].map(function (c) { 658 return '<div><code>' + c[0] + '</code><span>' + c[1] + '</span></div>'; }).join('') + '</div>'; 659 const col = function (w, big, small) { return '<div class="bx-col"><h3>' + w + '</h3>' + big + small.map(function (s) { return '<div class="bx-t">' + I(s[0]) + '<b>' + s[1] + '</b><span>' + s[2] + '</span></div>'; }).join('') + '</div>'; }; 660 body = '<div class="bx">' + 661 '<div class="bx-wide bx-mc"><b>Mission Control</b><span>One console for every endpoint and every company.</span><div class="la-chips">' + CONSOLE.map(function (x) { return '<span>' + x + '</span>'; }).join('') + '</div></div>' + 662 col('Predict.', '<div class="bx-big"><b>CVEs in real time, without scanning</b><span>Known-exploited first, from CISA KEV.</span>' + cve + '</div>', 663 [['user', 'Risky identities', 'Exposed accounts and paths to Tier 0.'], ['pulse', 'Security posture', 'A score from live criteria, per endpoint.']]) + 664 col('Stop.', '<div class="bx-big"><b>Ransomware, exploits and scripts blocked</b><span>On the endpoint, as they run.</span>' + tree + '</div>', 665 [['shield', 'Kill, quarantine, contain, revert', 'The response the attack needs, per threat.'], ['user', 'Identity attacks', 'Detected from the same agent.']]) + 666 col('Investigate.', '<div class="bx-big"><b>Every attack as one threat story</b><span>Mapped to MITRE ATT&CK.</span>' + chain + '</div>', 667 [['search', 'Remote forensic collection', 'Even from hosts that are offline.'], ['term', 'Live response and hunting', 'Any endpoint, any question.']]) + 668 '<div class="bx-wide bx-ag">' + logo + '<div><b>One lightweight agent</b><span>Windows, macOS and Linux. It deploys in minutes, even after a breach.</span></div></div></div>'; 669 } 670 671 const sec = document.createElement('section'); 672 sec.className = 'plx plx-' + mode + (still ? ' still' : ''); sec.id = 'pf'; 673 sec.innerHTML = '<div class="wrap plx-in"><div class="plx-head"><h2>Predict, Stop, & Investigate Attacks</h2><p class="plx-sub">One lightweight agent. <span>One single pane of glass.</span> Unlimited capabilities.</p></div>' + body + '</div>'; 674 hero.parentNode.insertBefore(sec, hero.nextSibling); 675 676 /* ---- motion for the two picture variants, only while on screen, none under reduced motion ---- */ 677 let inView = false, shown = false;
678 new IntersectionObserver(function (es) { 679 inView = es[0].isIntersecting; 680 if (inView && es[0].intersectionRatio > .2 && !shown) { shown = true; sec.classList.add('go'); } 681 }, { threshold: [0, .2, .4] }).observe(sec); 682 if (!still && (mode === 'pillars' || mode === 'pipeline')) sec.classList.add('pre'); 683 684 const NS = 'http://www.w3.org/2000/svg'; 685 const curve = function (a, b) { const m = (a[0] + b[0]) / 2; return 'M' + a[0].toFixed(1) + ',' + a[1].toFixed(1) + ' C' + m.toFixed(1) + ',' + a[1].toFixed(1) + ' ' + m.toFixed(1) + ',' + b[1].toFixed(1) + ' ' + b[0].toFixed(1) + ',' + b[1].toFixed(1); }; 686 const vcurve = function (a, b) { const m = (a[1] + b[1]) / 2; return 'M' + a[0].toFixed(1) + ',' + a[1].toFixed(1) + ' C' + a[0].toFixed(1) + ',' + m.toFixed(1) + ' ' + b[0].toFixed(1) + ',' + m.toFixed(1) + ' ' + b[0].toFixed(1) + ',' + b[1].toFixed(1); }; 687 function wired(box, svg, build) { 688 let dots = []; 689 function draw() { 690 if (window.innerWidth <= 1100) { svg.innerHTML = ''; dots = []; return; } 691 const r = box.getBoundingClientRect(); 692 const at = function (el, x, y) { const b = el.getBoundingClientRect(); return [b.left - r.left + b.width * x, b.top - r.top + b.height * y]; }; 693 svg.setAttribute('viewBox', '0 0 ' + r.width.toFixed(0) + ' ' + r.height.toFixed(0)); 694 const W = build(at); 695 svg.innerHTML = '<defs><marker id="plxArr" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,1 L9,5 L0,9 Z"/></marker></defs>' + 696 W.map(function (w) { return '<path class="w ' + (w.cls || '') + '" pathLength="1" d="' + w.d + '"' + (w.arrow ? ' marker-end="url(#plxArr)"' : '') + '/>'; }).join('') + 697 W.filter(function (w) { return w.node; }).map(function (w) { return '<circle class="nd" cx="' + w.node[0].toFixed(1) + '" cy="' + w.node[1].toFixed(1) + '" r="5"/>'; }).join('') + '<g class="dots"></g>'; 698 const g = svg.querySelector('.dots'); dots = []; 699 [].slice.call(svg.querySelectorAll('path.w')).forEach(function (p, j) { 700 const w = W[j]; if (!w.dots) return; 701 const len = p.getTotalLength(); 702 for (let k = 0; k < w.dots; k++) { const c = document.createElementNS(NS, 'circle'); c.setAttribute('r', 2.6); c.setAttribute('class', 'dot'); g.appendChild(c); dots.push({ el: c, p: p, len: len, u: k / w.dots + Math.random() * .15, sp: .16 + Math.random() * .08, rev: !!w.rev }); } 703 }); 704 } 705 draw(); 706 window.addEventListener('resize', draw); 707 document.addEventListener('site:ready', draw); 708 if (document.fonts) document.fonts.ready.then(draw); 709 if (still) return; 710 let last = 0; 711 (function loop(now) { 712 const dt = Math.min(64, now - (last || now)) / 1000; last = now; 713 if (inView && shown) dots.forEach(function (d) { 714 d.u = (d.u + dt * d.sp) % 1; 715 const pt = d.p.getPointAtLength((d.rev ? 1 - d.u : d.u) * d.len); 716 d.el.setAttribute('cx', pt.x.toFixed(1)); d.el.setAttribute('cy', pt.y.toFixed(1)); 717 d.el.style.opacity = Math.min(1, d.u * 5, (1 - d.u) * 5).toFixed(2); 718 }); 719 requestAnimationFrame(loop); 720 })(0); 721 } 722 723 if (mode === 'pillars') { 724 const box = sec.querySelector('.pi'), mc = box.querySelector('.pi-mc'), cards = box.querySelectorAll('.pi-card'); 725 /* Mission Control reaches down to every card; data rises back up the wires */ 726 wired(box, box.querySelector('.pi-wires'), function (at) { 727 return [].map.call(cards, function (c, i) { 728 const a = at(mc, .08 + i * .21, 1), b = at(c, .5, 0); 729 return { d: vcurve(a, b), node: a, dots: 1, rev: true }; 730 }); 731 }); 732 } 733 734 if (mode === 'pipeline') { 735 const box = sec.querySelector('.pp'), srcs = box.querySelectorAll('.pp-src'), pills = box.querySelectorAll('.pp-pill'); 736 const ring = box.querySelector('.pp-ring'), caps = box.querySelector('.pp-row'), mcx = box.querySelector('.pp-mc'); 737 wired(box, box.querySelector('.pp-wires'), function (at) { 738 const W = [], ag = at(ring, 0, .5); 739 [].forEach.call(srcs, function (s, i) { 740 const a = at(s, 1, .5), pill = pills[i], mid = [(a[0] + ag[0]) / 2 - 14, a[1] * .55 + ag[1] * .45]; 741 pill.style.left = mid[0].toFixed(1) + 'px'; pill.style.top = mid[1].toFixed(1) + 'px'; 742 W.push({ d: curve(a, mid) + ' ' + curve(mid, [ag[0] - 4, ag[1] + (i - 1) * 26]).replace(/^M[^C]+/, ''), node: a, dots: 2, arrow: true }); 743 }); 744 W.push({ d: 'M' + at(ring, 1, .5).map(function (v, k) { return (v + (k ? 0 : 6)).toFixed(1); }).join(',') + ' L' + at(caps, 0, .5).map(function (v, k) { return (v - (k ? 0 : 8)).toFixed(1); }).join(','), arrow: true, dots: 1, cls: 'hop' }); 745 W.push({ d: 'M' + at(caps, 1, .5).map(function (v, k) { return (v + (k ? 0 : 8)).toFixed(1); }).join(',') + ' L' + at(mcx, 0, .5).map(function (v, k) { return (v - (k ? 0 : 8)).toFixed(1); }).join(','), arrow: true, dots: 1, cls: 'hop' }); 746 return W; 747 }); 748 /* one part at a time, left to right: the sources, each capability in turn, then the console; the step 749 below follows what the lit part does (collect, detect, investigate, respond) */ 750 const steps = box.querySelectorAll('.pp-step'), capEls = box.querySelectorAll('.pp-cap'); 751 const SEQ = [['src', 0], [0, 1], [1, 1], [2, 1], [3, 2], [4, 1], ['mc', 3]]; 752 let k = -1; 753 function stepTo(i) { 754 k = i; 755 const s = SEQ[i]; 756 srcs.forEach(function (e) { e.classList.toggle('on', s[0] === 'src'); });
757 capEls.forEach(function (c, j) { c.classList.toggle('on', s[0] === j); }); 758 mcx.classList.toggle('on', s[0] === 'mc'); 759 steps.forEach(function (e, j) { e.classList.toggle('on', j === s[1]); }); 760 } 761 /* paused while the Screens section is carrying these cards away (it adds .carrying) */ 762 if (!still) setInterval(function () { if (inView && shown && !sec.classList.contains('carrying')) stepTo((k + 1) % SEQ.length); }, 1800); 763 } 764 765 /* ---- Screens (?shots=carry|orbit|off): the console itself, as the third part of the page. 766 Only built when neither the hero nor the after-hero already shows it. Takes the hero's tone, so the 767 three parts alternate. Nothing pins; carry's hand-over plays by itself once started (never rests half-way); orbit follows the scroll. 768 carry one big console window. As you scroll, the after-hero's own capability cards (Pillars or 769 Pipeline) come loose from their section and drift down into the window's tab strip; then each 770 tab lights in turn and its screen shows (click to choose). With Layers or Bento there are no 771 cards to carry, so the tabs themselves gather into the strip instead. 772 orbit every screen on a 3D carousel: the chosen one turns to the front at full size, the rest 773 wait behind it, turned away and dimmed; the list beside it moves on by itself */ 774 (function () { 775 const sm = root.getAttribute('data-shots'); 776 if (!sm || sm === 'off' || root.getAttribute('data-hero') === 'screen' || !window.PUI) return; 777 /* the screens are captures of the product's own pages (trmc-mockup, app menus hidden), in both themes; 778 the light hero tone gets the light ones. Capabilities with a workflow show it as steps. */ 779 const CAPS = [ 780 { k: 'edr', t: 'Endpoint Detection & Response', shots: [['edr-dashboard', 'dashboard', 'The dashboard: open threats, what needs a decision, and adversary behaviour mapped to MITRE ATT&CK.']] }, 781 { k: 'itdr', t: 'Identity Threat Protection', shots: [['itdr-overview', 'itdr / overview', 'Identity detections, lockout sources and the riskiest identities across the domain.']] }, 782 { k: 'threat', t: 'Threat Hunting', shots: [['hunt-results', 'hunt / processes', 'A hunt query across every endpoint: hidden-window PowerShell, with every match.']] }, 783 { k: 'ir', t: 'Incident Response & Forensics', shots: [['ir-liveview-cmd', 'live-view / WS-AISHA204', 'Live View: run a command on the endpoint, answered in seconds.'], 784 ['ir-liveview-proc', 'live-view / WS-AISHA204 / processes', 'Live View: every process on the endpoint, live.'], 785 ['ir-forensics', 'forensics / investigation', 'A forensic investigation across endpoints, mapped to MITRE ATT&CK.']] }, 786 { k: 'exp', t: 'Vulnerability & Exposure Detection', shots: [['exp-summary', 'exposure / summary', 'The exposure dashboard: what is actively exploited, and where.'], 787 ['exp-detail', 'exposure / details', 'One vulnerability, the endpoints it affects, and why.']] }]; 788 const ORDER = CAPS.map(function (c) { return c.k; }); 789 const SHOTS = []; 790 CAPS.forEach(function (c, i) { c.first = SHOTS.length; c.shots.forEach(function (s, j) { SHOTS.push({ c: i, j: j, n: c.shots.length, key: s[0], url: s[1], d: s[2] }); }); }); 791 const img = function (key) { 792 return '<img class="sx-img sx-d" src="/shared/screens/' + key + '-dark.webp" alt="" loading="lazy" decoding="async" width="1600" height="960">' + 793 '<img class="sx-img sx-l" src="/shared/screens/' + key + '-light.webp" alt="" loading="lazy" decoding="async" width="1600" height="960">'; 794 }; 795 const stepDots = function (s) { return s.n < 2 ? '' : '<span class="sx-steps">' + SHOTS.filter(function (x) { return x.c === s.c; }).map(function (x) { return '<i' + (x.j === s.j ? ' class="on"' : '') + '></i>'; }).join('') + '</span>'; }; 796 const tint = function (k) { const f = FIVE.find(function (c) { return c[0] === k; }); return f ? f[1] : 'b'; }; 797 const bar = function (u) { return '<div class="shx-bar"><i></i><i></i><i></i><span>console.threatresponder.com / <em>' + (u || '') + '</em></span></div>'; }; 798 const win = function (s) { return '<div class="shx-win">' + bar(s.url) + '<div class="shx-scr">' + img(s.key) + '</div></div>'; }; 799 const clamp = function (v) { return v < 0 ? 0 : v > 1 ? 1 : v; }; 800 const ease = function (k) { return 1 - Math.pow(1 - k, 3); }; 801 const io = function (k) { return k < .5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2; }; 802 const narrow = function () { return window.innerWidth <= 1100; }; 803 const HEAD = '<div class="shx-head"><h2>Inside Mission Control</h2><p>Every capability of the agent, in one console, for one company or all of them.</p></div>'; 804 805 /* ---- live player: the product's own pages, not pictures. The app mockups are served at /mockup/ next to 806 /website/ (same origin), so two iframes can load them with the app's header and menu hidden, scaled into 807 the window, and a script can drive them: the next page loads hidden and cross-fades in as soon as it has 808 drawn (its own loading effects play), then the step's actions run (typing a query, clicking Run, opening 809 a row) and it stays a moment. Three steps per capability. If /mockup/ can't be reached (e.g. a plain 810 local server), the screenshots above stay and cycle instead. */ 811 const APP = '/mockup/'; 812 /* Live View's process feed adds or ends a random process every 4.2 s; for the tour it ticks every 1.2 s and
813 its first two ticks start a process (conhost.exe, then cmd.exe: both sort near the top of the list) */ 814 const liveSpawns = function (d, w) { 815 const si = w.setInterval, R = w.Math.random, plan = [[0, 0], [0, 6.5 / 14]]; 816 w.setInterval = function (fn, ms) { 817 if (ms !== 4200) return si.apply(w, arguments); 818 return si.call(w, function () { 819 const q = plan.shift(); 820 if (q) { w.Math.random = function () { return q.length ? q.shift() : R(); }; } 821 try { fn(); } finally { w.Math.random = R; } 822 }, 1200); 823 }; 824 }; 825 const TOUR = [ 826 [{ page: 'dashboard', url: 'dashboard', d: 'The dashboard: open threats, what needs a decision, and MITRE ATT&CK at a glance.', stay: 2600 }, 827 { page: 'alerts-threats', url: 'alerts / threats', d: 'The threat queue: every threat, its kill chain, and what the agent already did.', stay: 2600 }, 828 { page: 'threat-story-full', bare: true, url: 'threats / story', d: 'Threat Story: the attack replayed step by step, mapped to MITRE ATT&CK.', run: [['wait', 700], ['click', '#playButton']], stay: 4600 }], 829 [{ page: 'itdr-overview', url: 'itdr / overview', d: 'Identity posture, detections and authentication across the domain.', stay: 2600 }, 830 { page: 'itdr-detections', url: 'itdr / detections', d: 'Identity detections, correlated and ranked by risk.', stay: 2800 }, 831 { page: 'lateral-movement', url: 'itdr / lateral-movement', d: 'Lateral movement: how an attacker reached Tier 0, replayed.', run: [['wait', 600], ['click', '#lmReplayAttack']], stay: 4200 }], 832 [{ page: 'hunt-process', q: '&dense=1', url: 'hunt / processes', d: 'Write a hunt query and run it across every endpoint.', run: [['wait', 250], ['type', '#hxInput', 'process.name(powershell.exe)', 22], ['wait', 150], ['run', '#hxRun']], stay: 2000 }, 833 { page: 'hunt-process', q: '&dense=1', same: true, url: 'hunt / processes', d: 'Open any match: the full process record, and where to pivot next.', run: [['click', '[data-exp]']], stay: 2200 }, 834 { page: 'hunt-network', url: 'hunt / network', d: 'Network hunts from the library: connections on a rare port.', run: [['wait', 350], ['click', '[data-runlib]']], stay: 2400 }], 835 [{ page: 'liveview-details', url: 'live-view / WS-AISHA204', d: 'Live View: run a command on the endpoint, answered in seconds.', run: [['wait', 250], ['type', '#lvxTermInput', 'netstat', 45], ['wait', 120], ['click', '#lvxTermRun']], stay: 2700 }, 836 { page: 'liveview-details', same: true, url: 'live-view / WS-AISHA204 / script', d: 'Write a script and run it on the endpoint.', run: [['click', '#lvxTermMode button:nth-child(2)'], ['wait', 350], 837 ['scroll', 200], ['cm', '.CodeMirror', "Get-Process | Where-Object { $_.Path -like '*\\Users\\Public\\*' } |\n Select-Object Name, Id, Path", 9], ['wait', 200], ['click', '#lvxScriptRun']], stay: 2400 }, 838 { page: 'liveview-details', same: true, url: 'live-view / WS-AISHA204 / processes', d: 'Every process on the endpoint, live: new ones pop up as they start.', run: [['scroll', 0], ['fn', liveSpawns], ['click', 'text:Processes']], stay: 4000 }], 839 [{ page: 'exposure-summary', url: 'exposure / summary', d: 'Exposure: what is actively exploited, where, and how fast it gets patched.', stay: 2600 }, 840 { page: 'exposure-details', url: 'exposure / details', d: 'Each vulnerability, the endpoints it affects, and why.', run: [['wait', 700], ['click', '[data-expand="0"]']], stay: 3200 }, 841 { page: 'exposure-misconfigs', url: 'exposure / misconfigurations', d: 'Misconfigurations that weaken each endpoint, ranked by impact.', stay: 2800 }]]; 842 const CHROME = '#meniuHeader,#nvSide,.page-footer-wrapper,#ovxDock,.ovx-dock,.nv-side{display:none!important}' + 843 'html body .page-container,html body .page-container .page-content-wrapper{padding-top:0!important;margin-top:0!important}' + 844 'html body .page-container .page-content-wrapper .page-content{margin-left:0!important;margin-top:0!important;min-height:0!important;border:0!important;border-radius:0!important}' + 845 'body{padding-top:0!important;overflow-x:hidden!important}::-webkit-scrollbar{display:none}html{scrollbar-width:none}'; 846 const tourDots = function (mi, si) { return '<span class="sx-steps">' + TOUR[mi].map(function (x, j) { return '<i' + (j === si ? ' class="on"' : '') + '></i>'; }).join('') + '</span>'; }; 847 /* vw: the width the app lays out at (the window scales it); narrower = larger text */ 848 function Player(layer, on, vw) { 849 vw = vw || 1600; 850 layer.innerHTML = '<iframe tabindex="-1" aria-hidden="true" title="ThreatResponder console"></iframe><iframe tabindex="-1" aria-hidden="true" title="ThreatResponder console"></iframe>'; 851 const F = [].slice.call(layer.querySelectorAll('iframe')), CANCEL = { c: 1 }, DEAD = { d: 1 }; 852 let front = 0, gen = 0, mi = 0, si = 0, dead = false, loaded = ''; 853 F.forEach(function (f) { f.style.width = vw + 'px'; f.style.height = Math.round(vw * .6) + 'px'; });
854 const fit = function () { const k = layer.clientWidth / vw; if (k) F.forEach(function (f) { f.style.transform = 'scale(' + k.toFixed(4) + ')'; }); }; 855 if (window.ResizeObserver) new ResizeObserver(fit).observe(layer); 856 window.addEventListener('resize', fit); fit(); 857 const theme = function () { return root.getAttribute('data-tone') === 'light' ? 'light' : 'dark'; }; 858 /* the hero tone switch flips the app's own theme in place */ 859 new MutationObserver(function () { 860 F.forEach(function (f) { 861 try { 862 const d = f.contentDocument, tb = d.getElementById('themeButton'); 863 /* the Threat Story page draws its graph for its own theme: flip it with its own button so it redraws */ 864 if (tb && d.documentElement.getAttribute('data-theme') !== theme()) tb.click(); 865 else d.documentElement.setAttribute('data-theme', theme()); 866 } catch (e) { /* not loaded */ } 867 }); 868 }) 869 .observe(root, { attributes: true, attributeFilter: ['data-tone'] }); 870 const sleep = function (ms, my) { return new Promise(function (res, rej) { setTimeout(function () { if (my === gen) res(); else rej(CANCEL); }, ms); }); }; 871 const wake = async function (my) { while (!on.visible() || document.hidden) await sleep(300, my); }; 872 /* the embedded app must never move the website: no scrolling into view, focus without scrolling */ 873 function prep(d) { 874 const s = d.createElement('style'); s.textContent = CHROME; d.head.appendChild(s); 875 const w = d.defaultView, P = w.HTMLElement.prototype, focus = P.focus; 876 P.scrollIntoView = function () {}; 877 P.focus = function () { focus.call(this, { preventScroll: true }); }; 878 d.documentElement.setAttribute('data-theme', theme()); 879 } 880 async function load(page, my, bare, q) { 881 const f = F[1 - front], want = page + '.html', t0 = Date.now(); 882 /* Static hosts may redirect name.html to name. Never accept a stale frame. */ 883 const canonical = function (url) { 884 const u = new URL(url, location.href); 885 886 return u.origin + u.pathname.replace(/\.html\/?$/, '').replace(/\/$/, '') + u.search; 887 }; 888 f.classList.remove('on'); 889 /* the Threat Story page takes its theme from its own saved setting (same origin), read before it draws */ 890 try { localStorage.setItem('tr-threat-story-theme', theme()); } catch (e) { /* storage blocked */ } 891 f.src = APP + want + '?theme=' + theme() + (q || ''); 892 for (;;) { 893 await sleep(40, my); 894 let d = null; 895 try { d = f.contentDocument; } catch (e) { throw DEAD; } 896 if (d && canonical(d.URL) === canonical(f.src) && d.body && d.readyState !== 'loading') { 897 if (d.querySelector('.page-content') || (bare && d.body.children.length > 1)) { prep(d); break; } 898 if (d.readyState === 'complete') throw DEAD; 899 } 900 if (Date.now() - t0 > 9000) throw DEAD; 901 } 902 await sleep(60, my); 903 f.classList.add('on'); F[front].classList.remove('on'); front = 1 - front; loaded = page; 904 on.live(); 905 } 906 function find(q) { 907 const d = F[front].contentDocument; 908 if (q === 'lib-play') return [].slice.call(d.querySelectorAll('.fa-play')).filter(function (e) { return e.offsetParent && !/Run on/.test(e.parentNode.textContent); })[0]; 909 if (q.indexOf('text:') === 0) { 910 const t = q.slice(5); 911 return [].slice.call(d.querySelectorAll('.page-content a, .page-content button, .page-content li, .page-content span, .page-content div')) 912 .filter(function (e) { return e.offsetParent && e.textContent.trim().indexOf(t) === 0; }) 913 .sort(function (a, b) { return a.textContent.length - b.textContent.length; })[0]; 914 } 915 return d.querySelector(q); 916 } 917 async function act(a, my) { 918 if (a[0] === 'wait') return sleep(a[1], my); 919 if (a[0] === 'fn') { a[1](F[front].contentDocument, F[front].contentWindow); return; } 920 if (a[0] === 'scroll') { F[front].contentWindow.scrollTo({ top: a[1], behavior: 'smooth' }); return sleep(450, my); } 921 /* pages draw in stages (Live View connects before its terminal exists): wait for the target */ 922 let el = find(a[1]); 923 for (let k = 0; !el && k < 50; k++) { await sleep(60, my); el = find(a[1]); } 924 const w = F[front].contentWindow; 925 if (!el) return; 926 if (a[0] === 'click') { el.click(); return sleep(150, my); } 927 if (a[0] === 'run') { el.disabled = false; el.click(); return sleep(150, my); } 928 /* a CodeMirror editor: type into its document */ 929 if (a[0] === 'cm' && el.CodeMirror) { 930 const cm = el.CodeMirror; 931 cm.setValue(''); 932 for (const ch of a[2]) { cm.replaceRange(ch, cm.posFromIndex(cm.getValue().length)); await sleep((a[3] || 30) * (.6 + Math.random() * .8), my); } 933 return; 934 } 935 if (a[0] === 'type') { 936 el.value = ''; 937 for (const ch of a[2]) { el.value += ch; el.dispatchEvent(new w.Event('input', { bubbles: true })); await sleep((a[3] || 30) * (.6 + Math.random() * .8), my); } 938 } 939 } 940 async function loop(my) { 941 try { 942 for (;;) { 943 await wake(my); 944 if (si === 0 && on.enter && await on.enter(mi, my, sleep)) loaded = ''; 945 const st = TOUR[mi][si]; 946 on.step(mi, si, st); 947 if (!st.same || loaded !== st.page) await load(st.page, my, st.bare, st.q); 948 for (const a of st.run || []) await act(a, my); 949 await sleep(st.stay, my); 950 if (++si >= TOUR[mi].length) { si = 0; mi = (mi + 1) % TOUR.length; } 951 } 952 } catch (e) { 953 if (e !== CANCEL) { 954 /* A failed embedded page must not leave an apparently running tour frozen. */ 955 console.warn('Mission Control live preview unavailable; continuing with screenshots.', e === DEAD ? 'Frame failed to load.' : e); 956 dead = true; layer.remove(); on.dead(); 957 } 958 } 959 } 960 return { 961 go: function (i) { if (dead) return; gen++; mi = i; si = 0; loop(gen); }, 962 get dead() { return dead; }, 963 get on() { return !dead && gen > 0; } 964 }; 965 } 966 967 const box = document.createElement('section'); 968 box.className = 'shx shx-' + sm + (still ? ' still' : ''); box.id = 'shx'; 969 if (sm === 'carry') { 970 box.innerHTML = '<div class="wrap shx-in">' + HEAD + '<div class="sxmg"><div class="sxmg-tabs" role="tablist" aria-label="Capabilities">' + CAPS.map(function (c, i) { 971 return '<button type="button" class="sxmg-tab ic-' + tint(c.k) + '" role="tab" data-i="' + i + '"><span class="sxmg-ic">
971' + icon(c.k) + '</span><b>' + c.t + '</b></button>'; 972 }).join('') + '</div>' + 973 '<div class="sxmg-stage"><div class="shx-win sxmg-win">' + bar('').replace('<em></em>', '<em class="sxmg-url"></em>') + 974 '<div class="sxmg-scr">' + SHOTS.map(function (s) { return '<div class="sxmg-s">' + img(s.key) + '</div>'; }).join('') + (still ? '' : '<div class="sx-live"></div>') + '</div></div></div>' + 975 '<p class="sxmg-cap" aria-live="polite"></p></div></div>'; 976 } 977 if (sm === 'orbit') { 978 box.innerHTML = '<div class="wide shx-in sxob-grid"><div class="sxob-side">' + HEAD + 979 '<ul class="sxob-list">' + CAPS.map(function (c, i) { 980 return '<li><button type="button" data-i="' + i + '"><span class="sxob-ic ic-' + tint(c.k) + '">' + icon(c.k) + '</span><b>' + c.t + '</b><i class="sxob-run"></i></button></li>'; 981 }).join('') + '</ul></div>' + 982 '<div class="sxob"><div class="sxob-stage">' + CAPS.map(function (c, i) { return '<div class="sxob-t" data-i="' + i + '">' + win(SHOTS[c.first]) + '</div>'; }).join('') + '</div>' + 983 '<div class="sxob-cap"><button type="button" class="sxob-nav" data-d="-1" aria-label="Previous screen">' + I('chevronL') + '</button>' + 984 '<div class="sxob-txt" aria-live="polite"></div><button type="button" class="sxob-nav" data-d="1" aria-label="Next screen">' + I('chevronR') + '</button></div></div></div>'; 985 } 986 sec.parentNode.insertBefore(box, sec.nextSibling); 987 box.querySelectorAll('.sxob-nav i').forEach(function (el) { 988 el.outerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="' + (el.getAttribute('data-ic') === 'chevronL' ? 'M14.5 6l-6 6 6 6' : 'M9.5 6l6 6-6 6') + '" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>'; 989 }); 990 991 let seen = false; 992 new IntersectionObserver(function (es) { seen = es[0].isIntersecting; }, { threshold: 0 }).observe(box); 993 994 /* one scroll-linked update per frame */ 995 function onScroll(cb) { 996 if (still) { cb(1); return; } 997 let queued = false; 998 const run = function () { queued = false; cb(null); }; 999 const req = function () { if (!queued) { queued = true; requestAnimationFrame(run); } }; 1000 window.addEventListener('scroll', req, { passive: true }); 1001 window.addEventListener('resize', req); 1002 document.addEventListener('site:ready', req); 1003 if (document.fonts) document.fonts.ready.then(req); 1004 req(); 1005 } 1006 /* page position from the layout, not the painted box: ignores the lift of a highlighted card and any 1007 transition still running, so a card never jumps when it comes loose */ 1008 function page(el) { 1009 let x = 0, y = 0, e = el; 1010 while (e) { x += e.offsetLeft; y += e.offsetTop; e = e.offsetParent; } 1011 return { x: x, y: y, w: el.offsetWidth, h: el.offsetHeight }; 1012 } 1013 /* played, not scrubbed (user, 24 Sep: the page must not rest with the cards half-way): once the page passes the 1014 start line the move runs to its end by itself in PLAY_MS, and back when you scroll above the line again; a 1015 40 px band keeps it from flickering on the line. The first frame jumps, so a reload mid-page does not replay. */ 1016 const PLAY_MS = 1300; 1017 function played(line, draw) { 1018 let p = -1, want = 0, from = 0, t0 = 0, dur = 0, raf = 0; 1019 const tick = function (now) { const k = dur ? clamp((now - t0) / dur) : 1; p = from + (want - from) * k; draw(p); raf = k < 1 ? requestAnimationFrame(tick) : 0; }; 1020 return function () { 1021 const l = line(), sy = window.scrollY, w = sy > l + 40 ? 1 : sy < l - 40 ? 0 : want; 1022 if (p < 0) { want = w; p = w; draw(p); return; } 1023 if (w !== want) { want = w; from = p; t0 = performance.now(); dur = PLAY_MS * Math.abs(want - from); if (!raf) raf = requestAnimationFrame(tick); } 1024 else if (!raf) draw(p); 1025 }; 1026 } 1027 1028 if (sm === 'carry') { 1029 const tabs = [].slice.call(box.querySelectorAll('.sxmg-tab')), wn = box.querySelector('.sxmg-win'), scr = box.querySelectorAll('.sxmg-s'); 1030 const url = box.querySelector('.sxmg-url'), cap = box.querySelector('.sxmg-cap'); 1031 let docked = false, cur = -1, hold = 0, last = Date.now(); 1032 /* show screen n; its capability's tab lights, the caption says what the screen shows */ 1033 function show(n, user) { 1034 if (user) hold = Date.now() + 15000; 1035 last = Date.now(); 1036 if (n === cur) return; 1037 cur = n; 1038 const s = SHOTS[n];
1039 tabs.forEach(function (t, j) { t.classList.toggle('on', j === s.c); t.setAttribute('aria-selected', j === s.c); }); 1040 scr.forEach(function (e, j) { e.classList.toggle('on', j === n); }); 1041 url.textContent = s.url; 1042 cap.innerHTML = '<b>' + CAPS[s.c].t + '.</b> ' + s.d + stepDots(s); 1043 } 1044 const layer = box.querySelector('.sx-live'); 1045 const player = layer && Player(layer, { 1046 visible: function () { return seen; }, 1047 live: function () { box.classList.add('live'); }, 1048 dead: function () { box.classList.remove('live'); last = Date.now(); }, 1049 step: function (mi, si, st) { 1050 tabs.forEach(function (t, j) { t.classList.toggle('on', j === mi); t.setAttribute('aria-selected', j === mi); }); 1051 url.textContent = st.url; 1052 cap.innerHTML = '<b>' + CAPS[mi].t + '.</b> ' + st.d + tourDots(mi, si); 1053 } 1054 }); 1055 tabs.forEach(function (t, i) { t.addEventListener('click', function () { if (player && player.on) player.go(i); else show(CAPS[i].first, true); }); }); 1056 show(0); 1057 /* the tour starts once the cards have landed */ 1058 const setDocked = function (d) { 1059 if (d === docked) return; 1060 docked = d; box.classList.toggle('docked', d); 1061 if (d && player && !player.on && !player.dead) player.go(0); 1062 }; 1063 const srcs = [].slice.call(document.querySelectorAll('#pf .pi-card, #pf .pp-cap')); 1064 1065 if (srcs.length === ORDER.length && !still) { 1066 /* copies of the after-hero's own cards live in a layer in page coordinates. While a card travels, the 1067 original is hidden with visibility (instant: the cards' own opacity transition made them blink) and 1068 the after-hero stops lighting its cards, so the copy always matches what it replaces. Each copy lifts 1069 (shadow grows, then settles), shrinks to its tab's width and, over the last stretch, is clipped to the 1070 tab's height as it hands over to the real tab underneath. Played, not scrubbed: it starts when the tab 1071 strip is a quarter of the way up the screen and runs to the end (or back) by itself. */ 1072 const layer = document.createElement('div'); 1073 layer.className = sec.className.replace(/\b(pre|go|still|carrying)\b/g, '') + ' sxc-layer'; 1074 layer.setAttribute('aria-hidden', 'true'); 1075 const ghosts = srcs.map(function (s) { const g = s.cloneNode(true); g.classList.remove('on'); g.removeAttribute('style'); layer.appendChild(g); return g; }); 1076 document.body.appendChild(layer); 1077 box.classList.add('carry-live'); 1078 let carrying = false; 1079 const play = played(function () { return page(tabs[0]).y - window.innerHeight * .75; }, function (p) { draw(p); }); 1080 onScroll(function () { 1081 if (narrow()) { 1082 ghosts.forEach(function (g) { g.style.visibility = 'hidden'; }); 1083 srcs.forEach(function (s) { s.style.visibility = ''; });
1084 tabs.forEach(function (t) { t.style.opacity = ''; }); 1085 box.classList.remove('carry-live'); sec.classList.remove('carrying'); carrying = false; 1086 setDocked(true); return; 1087 } 1088 box.classList.add('carry-live'); 1089 play(); 1090 }); 1091 function draw(p) { 1092 const A = srcs.map(page), B = tabs.map(page); 1093 const now = p > 0; 1094 if (now !== carrying) { 1095 carrying = now; sec.classList.toggle('carrying', now); 1096 if (now) srcs.forEach(function (s) { s.classList.remove('on'); }); 1097 } 1098 let all = true; 1099 srcs.forEach(function (s, i) { 1100 const e = io(clamp(p * 1.18 - i * .045)), g = ghosts[i], a = A[i], b = B[i], t = tabs[i]; 1101 if (e <= 0) { g.style.visibility = 'hidden'; s.style.visibility = ''; t.style.opacity = 0; all = false; return; } 1102 /* size changes only a little (Pipeline's cards are half a tab wide; scaling them to fit made them huge): 1103 each card flies to its tab's centre and the tab, wider or narrower, takes over around it */ 1104 const hand = clamp((e - .88) / .12), k = Math.min(1.08, Math.max(.9, b.w / a.w)), sc = 1 + (k - 1) * e, lift = Math.sin(Math.PI * Math.min(1, e / .9)); 1105 const cut = Math.max(0, a.h - (b.h / sc)) * hand; 1106 s.style.visibility = 'hidden'; 1107 g.style.width = a.w + 'px'; g.style.height = a.h + 'px'; 1108 const cx = a.x + a.w / 2 + (b.x + b.w / 2 - a.x - a.w / 2) * e; 1109 g.style.transform = 'translate(' + (cx - a.w * sc / 2).toFixed(1) + 'px,' + (a.y + (b.y - a.y) * e).toFixed(1) + 'px) scale(' + sc.toFixed(4) + ')'; 1110 g.style.clipPath = cut > .5 ? 'inset(0 0 ' + cut.toFixed(1) + 'px 0 round 14px)' : ''; 1111 g.style.filter = lift > .02 ? 'drop-shadow(0 ' + (18 * lift).toFixed(1) + 'px ' + (30 * lift).toFixed(1) + 'px rgba(5, 12, 45, ' + (.3 * lift).toFixed(3) + '))' : ''; 1112 /* the copy clears quickly while the tab comes up beneath it, so the two labels overlap only briefly */ 1113 const out = clamp(hand * 1.7); 1114 g.style.opacity = (1 - out).toFixed(3); g.style.visibility = out >= 1 ? 'hidden' : 'visible'; 1115 t.style.opacity = clamp(hand * 1.35).toFixed(3); 1116 if (e < 1) all = false; 1117 }); 1118 setDocked(all); 1119 } 1120 } else { 1121 /* no cards to carry (Layers or Bento after the hero): the tabs gather from a spread row into the strip */ 1122 const gather = function (p) { 1123 const mid = (tabs.length - 1) / 2;
1124 tabs.forEach(function (t, i) { 1125 const e = ease(clamp(p * 1.3 - i * .06)), u = 1 - e; 1126 t.style.transform = u < .002 ? '' : 'translate(' + ((i - mid) * 70 * u).toFixed(1) + 'px,' + (-34 * u).toFixed(1) + 'px) scale(' + (1 + .05 * u).toFixed(3) + ')'; 1127 }); 1128 const w = 1 - ease(p); wn.style.transform = p >= 1 ? '' : 'translateY(' + (90 * w).toFixed(1) + 'px) rotateX(' + (10 * w).toFixed(2) + 'deg)'; 1129 setDocked(p >= .999); 1130 }; 1131 const play = played(function () { return page(wn).y - window.innerHeight * .8; }, gather); 1132 onScroll(function (fixed) { if (fixed !== null) gather(fixed); else if (narrow()) gather(1); else play(); }); 1133 } 1134 if (!still) setInterval(function () { 1135 const now = Date.now(); 1136 if (player && !player.dead) return; 1137 if (!seen || !docked || now < hold) { last = Math.max(last, now - 1500); return; } 1138 if (now - last >= 4600) show((cur + 1) % SHOTS.length); 1139 }, 200); 1140 } 1141 1142 if (sm === 'orbit') { 1143 const wrap = box.querySelector('.sxob'), stage = box.querySelector('.sxob-stage'), tiles = [].slice.call(box.querySelectorAll('.sxob-t')); 1144 const items = [].slice.call(box.querySelectorAll('.sxob-list button')), txt = box.querySelector('.sxob-txt'), list = box.querySelector('.sxob-list'), n = tiles.length; 1145 const STEP = 4600, pos = tiles.map(function () { return null; }); 1146 let cur = -1, hold = 0, over = false, hoverT = 0; 1147 const X = function (d) { return 'translate3d(' + (d * 44) + '%, 0, ' + (-Math.abs(d) * 380) + 'px) rotateY(' + (-d * 36) + 'deg)' + (d ? ' scale(.84)' : ''); }; 1148 /* offset from the chosen screen: 0 in front, ±1 and ±2 turned away behind it, 3 out of sight. A screen 1149 that wraps round the back first appears, unseen, at the far edge it comes from, then turns in. */ 1150 function place(i, user) { 1151 if (user) hold = Date.now() + 12000; 1152 if (i === cur) return; 1153 const dir = cur < 0 ? 1 : (((i - cur) % n + n) % n <= n / 2 ? 1 : -1); 1154 cur = i; 1155 tiles.forEach(function (t, j) { 1156 let d = ((j - i) % n + n) % n; if (d > n / 2) d -= n; 1157 if (d === n / 2 && dir > 0) d = -n / 2; 1158 const was = pos[j]; 1159 if (was !== null && Math.abs(d - was) > 1) { 1160 const from = d > 0 ? 3 : -3; 1161 t.style.transition = 'none'; t.style.transform = X(from); t.setAttribute('data-a', 3); 1162 void t.offsetWidth; t.style.transition = ''; 1163 } 1164 pos[j] = d; 1165 t.style.transform = X(d); 1166 t.setAttribute('data-a', Math.abs(d)); 1167 t.style.zIndex = 10 - Math.abs(d); 1168 t.classList.toggle('on', d === 0); 1169 t.setAttribute('aria-hidden', d !== 0); 1170 }); 1171 const s = SHOTS[CAPS[i].first]; 1172 items.forEach(function (b, j) { b.classList.toggle('on', j === i); b.setAttribute('aria-pressed', j === i); }); 1173 if (!(player && player.on)) txt.innerHTML = '<b>' + CAPS[i].t + '</b><span>' + s.d + '</span>'; 1174 /* the running line under the chosen item shows when the carousel moves on */ 1175 list.classList.remove('run'); void list.offsetWidth; if (!still && !user && !(player && player.on)) list.classList.add('run'); 1176 } 1177 /* live: the front screen plays its capability's three steps, then the carousel turns to the next one */ 1178 const layer = still ? null : document.createElement('div'); 1179 let player = null; 1180 if (layer) { 1181 layer.className = 'sx-live'; 1182 player = Player(layer, { 1183 visible: function () { return seen; }, 1184 live: function () { box.classList.add('live'); layer.classList.add('show'); }, 1185 dead: function () { box.classList.remove('live'); }, 1186 enter: async function (i, my, sleep) { 1187 let moved = false; 1188 if (cur !== i) { layer.classList.remove('show'); place(i); await sleep(1150, my); } 1189 const host = tiles[i].querySelector('.shx-scr'); 1190 if (layer.parentNode !== host) { layer.classList.remove('show'); host.appendChild(layer); moved = true; } 1191 return moved; 1192 }, 1193 step: function (mi, si, st) {
1194 items.forEach(function (b, j) { b.classList.toggle('on', j === mi); b.setAttribute('aria-pressed', j === mi); }); 1195 txt.innerHTML = '<b>' + CAPS[mi].t + '</b><span>' + st.d + '</span>' + tourDots(mi, si); 1196 tiles[mi].querySelector('.shx-bar em').textContent = st.url; 1197 } 1198 }, 1280); 1199 } 1200 const pick = function (i) { if (player && player.on) player.go(i); else place(i, true); }; 1201 items.forEach(function (b, i) { 1202 /* a short settle before hover picks, so sweeping the mouse down the list doesn't spin the carousel */ 1203 const go = function () { if (cur !== i) pick(i); else hold = Date.now() + 12000; }; 1204 /* hover picks only in the picture mode; live pages load on click */ 1205 b.addEventListener('mouseenter', function () { clearTimeout(hoverT); if (!(player && player.on)) hoverT = setTimeout(go, 140); }); 1206 b.addEventListener('mouseleave', function () { clearTimeout(hoverT); }); 1207 b.addEventListener('focus', go); 1208 b.addEventListener('click', function () { clearTimeout(hoverT); go(); }); 1209 }); 1210 tiles.forEach(function (t, i) { t.addEventListener('click', function () { if (cur !== i) pick(i); }); }); 1211 box.querySelectorAll('.sxob-nav').forEach(function (b) { b.addEventListener('click', function () { pick((cur + (+b.getAttribute('data-d')) + n) % n); }); }); 1212 /* the carousel waits while the pointer is on it or on the list */ 1213 [wrap, list].forEach(function (el) { 1214 el.addEventListener('mouseenter', function () { over = true; list.classList.add('held'); }); 1215 el.addEventListener('mouseleave', function () { over = false; hold = Math.max(hold, Date.now() + 2500); list.classList.remove('held'); }); 1216 }); 1217 place(0); 1218 /* leans back while it comes up the page, level once in view */ 1219 onScroll(function (fixed) { 1220 let p = fixed; 1221 if (p === null) { const r = stage.getBoundingClientRect(), vh = window.innerHeight; p = narrow() ? 1 : clamp((vh - r.top) / (vh * .7)); } 1222 stage.style.setProperty('--p', ease(p).toFixed(3)); 1223 }); 1224 if (!still) { 1225 let last = Date.now(); 1226 setInterval(function () { 1227 const now = Date.now(); 1228 if (player && !player.dead) { 1229 if (!player.on && seen) player.go(0); 1230 return; 1231 } 1232 if (!seen || over || now < hold) { last = now; list.classList.remove('run'); return; } 1233 if (!list.classList.contains('run')) { list.classList.add('run'); last = now; } 1234 if (now - last >= STEP) { last = now; place((cur + 1) % n); } 1235 }, 200); 1236 } 1237 } 1238 })(); 1239 window.__pf = { mode: mode }; 1240 })(); 1241 1242 1243 /* ---- Predict Attacks Before they Occur (?predict=dash|window|off): the first of the headline's three verbs, after 1244 the Screens and before Neutralize (Stop). window (my first variant) has two halves, both drawn as the product sees them: 1245 exposure one week of one endpoint: every vulnerable application opens a CVE the moment it is installed 1246 and closes it the moment it is updated, each window kept on record; under it, what a weekly 1247 scan would have seen (one of three, days late). A cursor sweeps the week while in view. 1248 identity what the agent sees about accounts on every endpoint: new accounts, privilege, where 1249 privileged accounts log on, unusual logons, weak authentication; one event at a time. 1250 Takes the opposite tone of the Screens (a new chapter). Replaces the old Predict section. */ 1251 (function () { 1252 const root = document.documentElement; 1253 const hero = document.getElementById('hero'); 1254 if (!hero) return; 1255 const still = root.classList.contains('poster') || root.classList.contains('reduced'); 1256 const svg = function (p) { return '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">' + p + '</svg>'; }; 1257 const IC = { 1258 bug: '<rect x="7" y="8" width="10" height="12" rx="5"/><path d="M12 8V5M9 5l1.5 2M15 5l-1.5 2M4 12h3M17 12h3M4 17h3M17 17h3"/>', 1259 user: '<circle cx="12" cy="8" r="4"/><path d="M4 21c1.5-4 4.5-6 8-6s6.5 2 8 6"/>', 1260 check: '<path d="M5 12.5l4.5 4.5L19 7.5"/>' 1261 }; 1262 /* the week runs Mon 00:00 â next Mon 06:00 (174 h); the weekly scan runs Mon 02:00 */ 1263 const H = 174, at = function (d, h, m) { return ((d * 24 + h + (m || 0) / 60) / H * 100).toFixed(2); }; 1264 const EXP = [ 1265 { app: 'Google Chrome 124.0.6367.60', cve: 'CVE-2024-4671', kev: true, a: [1, 9, 14], b: [1, 15, 40], dur: '6 h 26 m', how: 'auto-updated' }, 1266 { app: '7-Zip 24.07', cve: 'CVE-2025-0411', kev: true, a: [2, 11, 2], b: [4, 10, 15], dur: '1 d 23 h', how: 'updated to 24.09' }, 1267 { app: 'Zoom 5.16.2', cve: 'CVE-2024-24691', kev: false, a: [3, 14, 30], b: null, dur: 'open', how: '' }]; 1268 const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon']; 1269 const IDN = [ 1270 { k: 'new', t: 'New local administrator', w: '<code>helpdesk2</code> created on <code>SRV-APP-02</code>' }, 1271 { k: 'priv', t: 'Privileged logon on a workstation', w: '<code>da-lopez</code> (Domain Admins) on <code>WS-FIN-07</code>' }, 1272 { k: 'beh', t: 'First logon at this hour', w: '<code>rharper</code> at 03:12 on <code>WS-RHARPER</code>' }, 1273 { k: 'weak', t: 'Weak authentication', w: '<code>hd-admin</code> used NTLMv1 on <code>WS-KOVAC112</code>' }, 1274 { k: 'svc', t: 'Service account used interactively', w: '<code>svc-backup</code> signed in on <code>WS-AISHA204</code>' }, 1275 { k: 'priv', t: 'Added to a privileged group', w: '<code>jmiller</code> joined Backup Operators' }]; 1276 const KIND = { new: 'Identity creation', priv: 'Privilege', beh: 'Behavior', weak: 'Authentication', svc: 'Usage' }; 1277 const TIMES = ['09:41', '10:03', '10:17', '10:52', '11:26', '11:40']; 1278 1279 /* ---- dash (the previous default, before the brief; after the user's reference picture): four numbers, a 30-day exposure heatmap 1280 (applications à days, colour = how many endpoints have the vulnerable version, a t
1280ick on the day it is 1281 fixed everywhere, status at the end of each row), a live identity table, and four capability cards. 1282 The numbers are an example organisation, said so; no "vs last week" arrows (rising risk read as bad news). */ 1283 const place = function (el) { 1284 const after = document.getElementById('shx') || document.getElementById('pf') || hero; 1285 after.parentNode.insertBefore(el, after.nextSibling); 1286 [].forEach.call(document.querySelectorAll('section.sec h2'), function (h) { if (/^Predict Attacks Before/.test(h.textContent)) h.closest('section').classList.add('pdx-old'); }); 1287 const tn = function () { 1288 const heroDark = root.getAttribute('data-tone') !== 'light'; 1289 el.classList.toggle('lt', document.getElementById('shx') ? heroDark : document.getElementById('pf') ? !heroDark : heroDark); 1290 }; 1291 tn(); 1292 new MutationObserver(tn).observe(root, { attributes: true, attributeFilter: ['data-tone'] }); 1293 }; 1294 /* ---- brief (default, 25 Sep 2026, after the user's picture): the homepage keeps the gist and the full dashboard 1295 (heatmap, identity table) lives on the platform page, where "See it in ThreatResponder" leads. Copy and a CTA on 1296 the left, a tilted summary card on the right (the official logo, four numbers of an example organisation, 1297 labelled sample data, and how many are already fixed), four capability cards below. ?predict=dash brings back 1298 the previous dashboard (only while the page is not SETTLED). */ 1299 if (!/^(dash|window)$/.test(root.getAttribute('data-predict') || '')) { 1300 const BI = { 1301 warn: '<path d="M12 4l9 16H3z"/><path d="M12 10v4M12 17h.01"/>', 1302 layers: '<path d="M12 3l9 5-9 5-9-5z"/><path d="M3 13l9 5 9-5"/>', 1303 user: IC.user, 1304 shield: '<path d="M12 3l7 3v6c0 4.2-3 7.7-7 9-4-1.3-7-4.8-7-9V6z"/>', 1305 bolt: '<path d="M13 3L5 13h6l-1 8 8-10h-6z"/>', 1306 aim: '<circle cx="12" cy="12" r="7.5"/><circle cx="12" cy="12" r="3.5"/><path d="M12 2.5v3M21.5 12h-3"/>', 1307 team: '<circle cx="9" cy="8" r="3.3"/><path d="M2.8 20c1-3.5 3.5-5.3 6.2-5.3s5.2 1.8 6.2 5.3"/><circle cx="17" cy="9" r="2.6"/><path d="M16.2 14.5c2.5-.2 4.4 1.3 5.1 4.3"/>', 1308 lock: '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/>', 1309 chev: '<path d="M9 6l6 6-6 6"/>', 1310 arrow: '<path d="M5 12h14M13 6l6 6-6 6"/>' 1311 }; 1312 const BK = [['warn', 'red', 127, 'Exposures detected'], ['layers', 'navy', 34, 'Known exploited (KEV)'], 1313 ['user', 'peri', 289, 'Identities watched'], ['shield', 'blue', 12, 'Critical risks']]; 1314 const BC = [['bolt', 'red', 'Real-time exposures', 'Open on install, closed on update. No scans.'], 1315 ['aim', 'navy', 'Known exploited first', 'CISA KEV and BOD 22-01 at the top.'], 1316 ['team', 'peri', 'Identity behavior', 'How each account is used, and where.'], 1317 ['lock', 'blue', 'Weak authentication', 'Legacy protocols and brute force, flagged.']]; 1318 const s3 = document.createElement('section'); 1319 s3.className = 'pdx pdb' + (still ? ' still' : ''); s3.id = 'pdx'; 1320 s3.innerHTML = '<div class="pdb-deco" aria-hidden="true"><svg class="pdb-sh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width=".9" stroke-linejoin="round">' + BI.shield + '</svg><i class="pdb-dots a"></i><i class="pdb-dots b"></i></div>' + 1321 '<div class="pdb-in"><div class="pdb-top">' + 1322 '<div class="pdb-copy"><p class="pdx-kick">Predict</p>' + 1323 '<h2>Predict Attacks <span>Before they Occur</span></h2>' + 1324 '<p class="pdb-lede">Spot vulnerable software and risky identities across your environment, and act before attackers can use them against you.</p>' + 1325 '<a class="pdb-cta" href="/threatresponder-platform/#predict">See it in ThreatResponder' + svg(BI.arrow) + '</a>' + 1326 '<p class="pdb-tags"><span>Real time</span><span>No scans</span><span>One lightweight agent</span></p></div>' + 1327 '<div class="pdb-vis" role="img" aria-label="Example summary, sample data: 127 exposures detected, 34 known exploited, 289 identities watched, 12 critical risks; 94 of 127 exposures already fixed.">' + 1328 '<div class="pdb-card" aria-hidden="true"><div class="pdb-ch"><span class="pdb-logo" data-logo="threatresponder"></span><span class="pdb-sample">Sample data</span></div>' + 1329 '<ul class="pdb-kp">' + BK.map(function (k, i) { 1330 return '<li style="--r:' + i + '"><span class="pdb-ki t-' + k[1] + '">' + svg(BI[k[0]]) + '</span><span class="pdb-kt"><b data-n="' + k[2] + '">' + (still ? k[2] : 0) + '</b><span>' + k[3] + '</span></span>' + svg(BI.chev) + '</li>'; 1331 }).join('') + '</ul></div>' + 1332 '<div class="pdb-float" aria-hidden="true"><span class="pdb-fi">' + svg(IC.check) + '</span><span class="pdb-ft"><b>94 of 127 fixed</b><span class="pdb-bar"><i></i></span></span></div></div>' + 1333 '</div><ul class="pdb-cards">' + BC.map(function (c) { 1334 return '<li><span class="pdb-ci t-' + c[1] + '">' + svg(BI[c[0]]) + '</span><span><b>' + c[2] + '</b><small>' + c[3] + '</small></span></li>'; 1335 }).join('') + '</ul></div>'; 1336 place(s3); 1337 if (window.Logos) window.Logos.paint(s3); 1338 /* once in view: the card settles in, its rows follow, the numbers count up, then the fixed badge */ 1339 if (still) { s3.classList.add('go'); return; } 1340 const io3 = new IntersectionObserver(function (en) { 1341 if (!en[0].isIntersecting) return; 1342 io3.disconnect(); s3.classList.add('go'); 1343 s3.querySelectorAll('.pdb-kp b').forEach(function (b, i) { 1344 const n = +b.dataset.n; 1345 setTimeout(function () { 1346 const t0 = performance.now(); 1347 (function f(now) { const u = Math.min(1, (now - t0) / 1200); b.textContent = Math.round(n * (1 - Math.pow(1 - u, 3))); if (u < 1) requestAnimationFrame(f); })(t0); 1348 }, 350 + i * 120); 1349 }); 1350 }, { threshold: .3 }); 1351 io3.observe(s3.querySelector('.pdb-vis')); 1352 return; 1353 } 1354 if (root.getAttribute('data-predict') !== 'window') { 1355 const DI = { 1356 warn: '<path d="M12 4l9 16H3z"/>
1356<path d="M12 10v4M12 17h.01"/>', 1357 layers: '<path d="M12 3l9 5-9 5-9-5z"/><path d="M3 13l9 5 9-5"/>', 1358 user: IC.user, 1359 shield: '<path d="M12 3l7 3v6c0 4.2-3 7.7-7 9-4-1.3-7-4.8-7-9V6z"/>', 1360 bolt: '<path d="M13 3L5 13h6l-1 8 8-10h-6z"/>', 1361 aim: '<circle cx="12" cy="12" r="7.5"/><circle cx="12" cy="12" r="3.5"/><path d="M12 2.5v3M21.5 12h-3"/>', 1362 team: '<circle cx="9" cy="8" r="3.3"/><path d="M2.8 20c1-3.5 3.5-5.3 6.2-5.3s5.2 1.8 6.2 5.3"/><circle cx="17" cy="9" r="2.6"/><path d="M16.2 14.5c2.5-.2 4.4 1.3 5.1 4.3"/>', 1363 lock: '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/>' 1364 }; 1365 const KPI = [['warn', 127, 'Exposures detected', '94 already fixed'], ['layers', 34, 'Known exploited (KEV)', 'fixed first'], 1366 ['user', 289, 'Identities watched', 'on 1,240 endpoints'], ['shield', 12, 'Critical risks', '3 need a decision']]; 1367 /* 30 days, today last; per application: endpoints with the vulnerable version by day (0 = none) */ 1368 const D0 = new Date(2026, 7, 26), DAYN = 30; 1369 const day = function (i) { const d = new Date(D0); d.setDate(d.getDate() + i); return d; }; 1370 const fmt = function (d) { return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); }; 1371 const HM = [ 1372 { app: 'Google Chrome', cve: 'CVE-2024-4671', kev: true, m: 'G', d: { 3: 3, 4: 9, 5: 24, 6: 31, 7: 18, 8: 6, 9: 2 }, fix: 10 }, 1373 { app: '7-Zip', cve: 'CVE-2025-0411', kev: true, m: '7z', d: { 8: 2, 9: 4, 10: 7, 11: 12, 12: 16, 13: 22, 14: 26, 15: 25, 16: 19, 17: 11, 18: 6, 19: 3, 20: 1 }, fix: 21 }, 1374 { app: 'OpenSSL', cve: 'CVE-2024-5535', kev: false, m: 'SSL', d: { 12: 3, 13: 3, 14: 4, 15: 4, 16: 5, 17: 5, 18: 4, 19: 3, 20: 2, 21: 2, 22: 1 }, fix: 23 }, 1375 { app: 'Apache Tomcat', cve: 'CVE-2024-50379', kev: false, m: 'Tc', d: { 15: 4, 16: 8, 17: 9, 18: 9, 19: 5, 20: 2 }, fix: 21 }, 1376 { app: 'Microsoft Windows', cve: 'CVE-2024-21338', kev: true, m: 'W', d: { 21: 6, 22: 14, 23: 27, 24: 38, 25: 33, 26: 21, 27: 12, 28: 8, 29: 5 }, fix: null }]; 1377 const lvl = function (n) { return !n ? 0 : n <= 5 ? 1 : n <= 20 ? 2 : 3; }; 1378 const IDT = [ 1379 ['09:41', 'New local administrator', '<code>jdoe</code> on <code>WKSTN-004</code>', 'priv', 'Privilege'], 1380 ['11:26', 'Service account used interactively', '<code>svc-backup</code> on <code>FILE-02</code>', 'svc', 'Unusual use'], 1381 ['13:03', 'Multiple failed logons', '<code>m.smith</code> from <code>203.0.113.24</code>', 'weak', 'Brute force'], 1382 ['15:17', 'Sign-in from a new country', '<code>c.wilson</code> from Germany', 'beh', 'Unusual location'], 1383 ['17:02', 'Weak authentication method', '<code>legacy-app</code> using NTLMv1', 'new', 'Weak auth'], 1384 ['17:40', 'Added to a privileged group', '<code>jmiller</code> joined Backup Operators', 'priv', 'Privilege'], 1385 ['18:05', 'Domain admin on a workstation', '<code>da-lopez</code> on <code>WS-FIN-07</code>', 'priv', 'Exposure']]; 1386 const CARDS = [['bolt', 'Real-time exposure visibility', 'A vulnerability appears when the software is installed and closes when it is fixed. No scans.'], 1387 ['aim', 'Known exploited, prioritized', 'CISA KEV and BOD 22-01, straight to the top.'], 1388 ['team', 'Identity behavior context', 'How each account is used, where it signs in, and when that changes.'], 1389 ['lock', 'Weak authentication detection', 'Legacy protocols, brute force, and service accounts used by people.']]; 1390 const irow = function (r) { 1391 return '<div class="pdd-rw"><div class="pdd-ir"><span class="pdd-t">' + r[0] + '</span><span class="pdd-e"><b>' + r[1] + '</b><span>' + r[2] + '</span></span><span class="pdd-k k-' + r[3] + '">' + r[4] + '</span></div></div>'; 1392 }; 1393 const s2 = document.createElement('section'); 1394 s2.className = 'pdx pdd' + (still ? ' still' : ''); s2.id = 'pdx'; 1395 s2.innerHTML = '<div class="pdd-in">' + 1396 '<div class="pdx-head"><p class="pdx-kick">Predict</p><h2>Predict Attacks Before they Occur</h2>' + 1397 '<p>Spot vulnerable software and risky identities across your environment before attackers use them against you.</p></div>' + 1398 '<div class="pdd-kpis">' + KPI.map(function (k, i) { 1399 return '<div class="pdd-kpi"><span class="pdd-ki' + (i ? '' : ' r') + '">' + svg(DI[k[0]]) + '</span><div><b data-n="' + k[1] + '">' + (still ? k[1] : 0) + '</b><span>' + k[2] + '</span><small>' + k[3] + '</small></div></div>'; 1400 }).join('') + '</div>' + 1401 '<div class="pdd-grid">' + 1402 '<article class="pdx-p"><div class="pdx-ph"><span class="pdx-ic">' + svg(IC.bug) + '</span><div><h3>Vulnerabilities, the moment they appear</h3><p>When vulnerable software is introduced, how far it spreads, and when it is fixed.</p></div><span class="pdd-when">Last 30 days</span></div>' + 1403 '<div class="pdd-hm" aria-hidden="true" style="--n:' + DAYN + '"><div class="pdd-hh"><span>Application / CVE</span><span class="pdd-ax">' + 1404 [0, 4, 8, 12, 16, 20].map(function (i) { return '<i' + (i % 8 ? ' class="o"' : '') + ' style="--i:' + i + '">' + fmt(day(i)) + '</i>'; }).join('') + '<i class="pdd-today" style="--i:' + (DAYN - 1) + '">Today</i></span><span>Status</span></div>' + 1405 HM.map(function (h) { 1406 let cells = ''; 1407 for (let i = 0; i < DAYN; i++) { 1408 const n = h.d[i] || 0, fx = h.fix === i; 1409 cells += '<i class="c' + lvl(n) + (fx ? ' fx' : '') + '" style="--i:' + i + '"' + (n ? ' title="' + n + ' endpoints"' : '') + '>
1409' + (fx ? svg(IC.check) : '') + '</i>'; 1410 } 1411 const peak = Math.max.apply(null, Object.keys(h.d).map(function (k) { return h.d[k]; })); 1412 const first = Math.min.apply(null, Object.keys(h.d).map(Number)); 1413 const st = h.fix !== null ? '<span class="pdd-st ok">Fixed · ' + (h.fix - first) + ' d</span>' : '<span class="pdd-st open">Open · ' + h.d[DAYN - 1] + ' endpoints</span>'; 1414 return '<div class="pdd-r"><span class="pdd-app"><span class="pdd-m">' + h.m + '</span><span><b>' + h.app + (h.kev ? '<em>KEV</em>' : '') + '</b><small>' + h.cve + ' · peak ' + peak + '</small></span></span>' + 1415 '<span class="pdd-cells">' + cells + '</span>' + st + '</div>'; 1416 }).join('') + 1417 '<div class="pdd-lg"><span><i class="c0"></i>Not present</span><span><i class="c1"></i>1â5 endpoints</span><span><i class="c2"></i>6â20</span><span><i class="c3"></i>21+</span><span><i class="fx">' + svg(IC.check) + '</i>Fixed everywhere</span></div></div></article>' + 1418 '<article class="pdx-p"><div class="pdx-ph"><span class="pdx-ic">' + svg(IC.user) + '</span><div><h3>Identities, seen from every endpoint</h3><p>How accounts are used, where they sign in, and when risky behavior appears.</p></div></div>' + 1419 '<div class="pdd-it" aria-hidden="true"><div class="pdd-ih"><span>Time</span><span>Identity / event</span><span class="pdx-live"><i></i>Live</span></div><div class="pdd-rows">' + 1420 IDT.slice(0, 5).reverse().map(irow).join('') + '</div></div></article></div>' + 1421 '<ul class="pdd-cards">' + CARDS.map(function (c) { return '<li><span>' + svg(DI[c[0]]) + '</span><div><b>' + c[1] + '</b><small>' + c[2] + '</small></div></li>'; }).join('') + '</ul></div>'; 1422 place(s2); 1423 1424 /* reveal once in view: numbers count up, the heatmap fills day by day up to Today, then the status; 1425 the identity table takes a new event every 3 s (five rows, fixed height) */ 1426 const rows2 = s2.querySelector('.pdd-rows'); 1427 const fix2 = function () { if (rows2.children.length !== 5 || rows2.querySelector('.shut')) return; rows2.style.height = ''; rows2.style.height = rows2.offsetHeight + 'px'; }; 1428 fix2(); 1429 window.addEventListener('resize', function () { rows2.style.height = ''; setTimeout(fix2, 700); }); 1430 document.addEventListener('site:ready', fix2); 1431 if (document.fonts) document.fonts.ready.then(fix2); 1432 if (still) { s2.classList.add('go'); return; } 1433 let k2 = 5, t2 = 0, seen2 = false, in2 = false; 1434 function next2() { 1435 clearTimeout(t2); if (!in2) return; 1436 rows2.insertAdjacentHTML('afterbegin', irow(IDT[k2 % IDT.length]).replace('pdd-rw', 'pdd-rw shut fresh')); k2++; 1437 const r = rows2.firstElementChild; void r.offsetHeight; r.classList.remove('shut'); 1438 setTimeout(function () { r.classList.remove('fresh'); }, 1800); 1439 const old = rows2.children[rows2.children.length - 1]; old.classList.add('shut'); setTimeout(function () { old.remove(); }, 650); 1440 t2 = setTimeout(next2, 3000); 1441 } 1442 new IntersectionObserver(function (en) { 1443 const was = in2; in2 = en[0].isIntersecting && en[0].intersectionRatio > .25; 1444 if (in2 && !seen2) { 1445 seen2 = true; s2.classList.add('go'); 1446 s2.querySelectorAll('.pdd-kpi b').forEach(function (b) { 1447 const n = +b.dataset.n, t0 = performance.now(); 1448 (function f(now) { const u = Math.min(1, (now - t0) / 1400); b.textContent = Math.round(n * (1 - Math.pow(1 - u, 3))); if (u < 1) requestAnimationFrame(f); })(t0); 1449 }); 1450 } 1451 if (in2 && !was) t2 = setTimeout(next2, 2600); 1452 if (!in2) clearTimeout(t2); 1453 }, { threshold: [0, .25, .5] }).observe(s2.querySelector('.pdd-grid')); 1454 return; 1455 } 1456 1457 const sec = document.createElement('section'); 1458 sec.className = 'pdx' + (still ? ' still' : ''); sec.id = 'pdx'; 1459 sec.innerHTML = '<div class="pdx-in">' + 1460 '<div class="pdx-head"><p class="pdx-kick">Predict</p><h2>Predict Attacks Before they Occur</h2>' + 1461 '<p>See what an attacker could use before they use it: every vulnerable application and every risky account, on every endpoint, as it changes.</p></div>' + 1462 '<div class="pdx-grid">' + 1463 /* exposure */ 1464 '<article class="pdx-p"><div class="pdx-ph"><span class="pdx-ic">' + svg(IC.bug) + '</span><div><h3>Vulnerabilities, the moment they appear</h3><p>No scans. A CVE opens when vulnerable software is installed and closes when it is updated.</p></div></div>' + 1465 '<div class="pdx-wk" aria-hidden="true"><div class="pdx-wk-top"><span class="pdx-host"><code>WS-AISHA204</code></span><span class="pdx-days">' + 1466 DAYS.map(function (d, i) { return '<i style="left:' + at(i, 0) + '%">' + d + '</i>'; }).join('') + '</span></div>' + 1467 '<div class="pdx-lane pdx-tr"><span class="pdx-ln">ThreatResponder</span><div class="pdx-track">' + 1468 EXP.map(function (e, i) { 1469 const a = +at(e.a[0], e.a[1], e.a[2]), b = e.b ? +at(e.b[0], e.b[1], e.b[2]) : 100; 1470 return '<div class="pdx-x' + (e.b ? '' : ' open') + '" data-a="' + a + '" data-b="' + b + '" style="--a:' + a + '%;--b:' + b + '%;--r:' + i + '">' + 1471 '<span class="pdx-bar"></span><span class="pdx-tag"><b>' + e.cve + '</b>' + (e.kev ? '<em>KEV</em>' : '') + '<small>' + e.app + '</small></span>' + 1472 '<span class="pdx-end">' + (e.b ? svg(IC.check) + 'Closed · ' + e.dur : 'Still open') + '</span></div>'; 1473 }).join('') + '</div></div>' + 1474 '<div class="pdx-lane pdx-sc"><span class="pdx-ln">Weekly scan</span><div class="pdx-track">' + 1475 '<span class="pdx-scan" style="left:' + at(0, 2) + '%" data-a="' + at(0, 2) + '"><i></i>0 found</span>' + 1476 '<span class="pdx-scan" style="left:' + at(7, 2) + '%" data-a="' + at(7, 2) + '"><i></i>1 found</span></div></div>' + 1477 '<div class="pdx-cur"></div></div>' + 1478 '<p class="pdx-note" aria-hidden="true"><span class="pdx-note-a">Every window on record: opened, closed, and how.</span><span class="pdx-note-b">A weekly scan saw 1 of 3, four days late.</span></p>' + 1479 '<ul class="pdx-f"><li><b>Real-time CVE detection</b><span>Opened and closed the moment software changes.</span></li>' + 1480 '<li><b>Known-exploited first</b><span>CISA KEV and BOD 22-01, straight to the top.</span></li>' + 1481 '<li><b>Remediation tracking</b><span>
1481Every exposure window kept as evidence.</span></li></ul></article>' + 1482 /* identity */ 1483 '<article class="pdx-p"><div class="pdx-ph"><span class="pdx-ic">' + svg(IC.user) + '</span><div><h3>Identities, seen from every endpoint</h3><p>Not only at the domain controller: the agent sees every account where it is used.</p></div></div>' + 1484 '<div class="pdx-id" aria-hidden="true"><div class="pdx-id-h"><span>Identity activity</span><span class="pdx-live"><i></i>Live</span></div><div class="pdx-rows"></div></div>' + 1485 '<ul class="pdx-f"><li><b>Identity creation & usage</b><span>New accounts, privilege changes and where each account logs on.</span></li>' + 1486 '<li><b>User behavior</b><span>Unusual logons, hours and sessions, per user.</span></li>' + 1487 '<li><b>Risky authentication</b><span>Weak protocols and service accounts used by people.</span></li></ul></article>' + 1488 '</div></div>'; 1489 const after = document.getElementById('shx') || document.getElementById('pf') || hero; 1490 after.parentNode.insertBefore(sec, after.nextSibling); 1491 /* the live site's own Predict section is replaced by this one */ 1492 [].forEach.call(document.querySelectorAll('section.sec h2'), function (h) { if (/^Predict Attacks Before/.test(h.textContent)) h.closest('section').classList.add('pdx-old'); }); 1493 1494 /* ---- tone: a new chapter, so the opposite of the Screens (or of whatever comes before) ---- */ 1495 function tone() { 1496 const heroDark = root.getAttribute('data-tone') !== 'light'; 1497 const prevDark = document.getElementById('shx') ? heroDark : document.getElementById('pf') ? !heroDark : heroDark; 1498 sec.classList.toggle('lt', prevDark); 1499 } 1500 tone(); 1501 new MutationObserver(tone).observe(root, { attributes: true, attributeFilter: ['data-tone'] }); 1502 1503 /* ---- identity feed: newest on top, four rows, fixed height ---- */ 1504 const rows = sec.querySelector('.pdx-rows'); 1505 let n = 0; 1506 function add(fresh) { 1507 const e = IDN[n % IDN.length], tm = TIMES[n % TIMES.length]; n++; 1508 rows.insertAdjacentHTML('afterbegin', '<div class="pdx-rw' + (fresh ? ' shut fresh' : '') + '"><div class="pdx-r k-' + e.k + '"><span class="pdx-t">' + tm + '</span>' + 1509 '<span class="pdx-w"><b>' + e.t + '</b><span>' + e.w + '</span></span><span class="pdx-k">' + KIND[e.k] + '</span></div></div>'); 1510 const r = rows.firstElementChild; 1511 if (fresh) { void r.offsetHeight; r.classList.remove('shut'); setTimeout(function () { r.classList.remove('fresh'); }, 1800); } 1512 const all = rows.children; 1513 if (all.length > 4) { const old = all[all.length - 1]; if (fresh) { old.classList.add('shut'); setTimeout(function () { old.remove(); }, 650); } else old.remove(); } 1514 } 1515 for (let i = 0; i < 4; i++) add(false); 1516 function fixH() { if (rows.children.length !== 4 || rows.querySelector('.shut')) return; rows.style.height = ''; rows.style.height = rows.offsetHeight + 'px'; } 1517 fixH(); 1518 window.addEventListener('resize', function () { rows.style.height = ''; setTimeout(fixH, 700); }); 1519 document.addEventListener('site:ready', fixH); 1520 if (document.fonts) document.fonts.ready.then(fixH); 1521 1522 /* ---- the week: a cursor sweeps it; windows open and close as it passes; the scans fire at their hour ---- */ 1523 const wk = sec.querySelector('.pdx-wk'), xs = [].slice.call(sec.querySelectorAll('.pdx-x')), scans = [].slice.call(sec.querySelectorAll('.pdx-scan')); 1524 const note = sec.querySelector('.pdx-note'); 1525 function paint(t) { 1526 wk.style.setProperty('--tn', (t / 100).toFixed(4)); 1527 xs.forEach(function (x) { 1528 const a = +x.dataset.a, b = +x.dataset.b; 1529 x.style.setProperty('--w', Math.max(0, Math.min(t, b) - a) + '%'); 1530 x.classList.toggle('on', t >= a); x.classList.toggle('done', t >= b && !x.classList.contains('open')); 1531 }); 1532 scans.forEach(function (s) { s.classList.toggle('on', t >= +s.dataset.a); }); 1533 note.classList.toggle('on', t >= 99.5); 1534 } 1535 if (still) { paint(100); return; } 1536 paint(0); 1537 let inView = false, t0 = 0, raf = 0, idTimer = 0; 1538 const SWEEP = 9000, HOLD = 4200; 1539 function frame(now) { 1540 if (!inView) { raf = 0; return; } 1541 if (!t0) t0 = now; 1542 const e = now - t0; 1543 if (e < SWEEP) paint(e / SWEEP * 100); 1544 else if (e < SWEEP + HOLD) paint(100); 1545 else { wk.classList.add('reset'); paint(0); t0 = now; setTimeout(function () { wk.classList.remove('reset'); }, 60); } 1546 raf = requestAnimationFrame(frame); 1547 } 1548 function idLoop() { clearTimeout(idTimer); if (!inView) return; add(true); idTimer = setTimeout(idLoop, 2600); } 1549 new IntersectionObserver(function (en) { 1550 const was = inView; inView = en[0].isIntersecting && en[0].intersectionRatio > .3; 1551 if (inView && !was) { t0 = 0; if (!raf) raf = requestAnimationFrame(frame); idTimer = setTimeout(idLoop, 1400); } 1552 if (!inView) clearTimeout(idTimer); 1553 }, { threshold: [0, .3, .5] }
1553).observe(sec.querySelector('.pdx-grid')); 1554 })(); 1555 1556 /* ---- Neutralize Threats in Real Time (?engines=pair|flip|off): the live site's detection-engines section, 1557 straight after the Screens: the six engines of the agent (names and items as on netsecurity.com), then 1558 the four outcomes. Static (the user dropped the wired alert queue below the cards). 1559 Tone: pair = the same tone as the section before it (Screens and Engines read as one product chapter, 1560 on a different surface: the corridor picture), flip = strict alternation. The section after it always 1561 takes the other tone. Replaces the Yardstick section, the proof strip and the old Neutralize section. */ 1562 (function () { 1563 const root = document.documentElement; 1564 const hero = document.getElementById('hero'); 1565 if (!hero) return; 1566 const svg = function (p) { return '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">' + p + '</svg>'; }; 1567 const IC = { 1568 intel: '<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17M12 3.5c2.8 3.3 2.8 13.7 0 17M12 3.5c-2.8 3.3-2.8 13.7 0 17"/>', 1569 files: '<path d="M7 3h7l5 5v13H7z"/><path d="M14 3v5h5"/><path d="M10.5 12.5l-2 2 2 2M14 12.5l2 2-2 2"/>', 1570 behav: '<rect x="4" y="5" width="16" height="11" rx="1.5"/><path d="M2 19.5h20"/><path d="M7 11h2.5l1.5-3 2 5 1.5-2H17"/>', 1571 net: '<circle cx="12" cy="5" r="2.2"/><circle cx="5" cy="18.5" r="2.2"/><circle cx="19" cy="18.5" r="2.2"/><circle cx="12" cy="12.5" r="2.2"/><path d="M12 7.2v3.1M10.2 13.9l-3.4 2.8M13.8 13.9l3.4 2.8"/>', 1572 exploit: '<path d="M12 3l7 3v6c0 4.2-3 7.7-7 9-4-1.3-7-4.8-7-9V6z"/><path d="M12 8v5M12 16.2h.01"/>', 1573 rules: '<path d="M4 7h9M17 7h3M4 12h5M13 12h7M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="11" cy="12" r="2"/><circle cx="9" cy="17" r="2"/>', 1574 clock: '<circle cx="12" cy="12" r="8.5"/><path d="M12 7v5l3 2"/>', 1575 aim: '<circle cx="12" cy="12" r="6.5"/><path d="M12 2.5v4M12 17.5v4M2.5 12h4M17.5 12h4"/><circle cx="12" cy="12" r="1.4"/>', 1576 team: '<circle cx="9" cy="8" r="3.3"/><path d="M2.8 20c1-3.5 3.5-5.3 6.2-5.3s5.2 1.8 6.2 5.3"/><circle cx="17" cy="9" r="2.6"/><path d="M16.2 14.5c2.5-.2 4.4 1.3 5.1 4.3"/>', 1577 rise: '<path d="M4 20h16M7 20v-4M11.5 20v-7M16 20V10"/><path d="M5 11.5l5-4 3 2 5.5-5M15 4.5h3.5V8"/>', 1578 arrow: '<path d="M5 12h14M13 6l6 6-6 6"/>' 1579 }; 1580 /* the six engines, from the live site (TRMC, the internal name, reads "the agent" here). Compact (25 Sep 2026): the homepage 1581 shows each engine's name and one line; the lists (f) are shown on the platform page (#engines), linked below the cards. 1582 The sixth is "Custom Detection Rules" (user): Sigma, YARA and regex rules are added or uploaded in the console. */ 1583 const ENG = [ 1584 { ic: 'intel', t: 'Threat Intelligence & Reputation', d: 'Known indicators, reputation and cloud intelligence.', 1585 f: [['File Reputation & Cloud Lookup', 'Known malicious file intelligence.'], ['Malicious IP Indicators', 'Curated IPv4 and IPv6 intelligence.'], 1586 ['Malicious Domain Indicators', 'Malicious and phishing-domain intelligence.'], ['Newly Registered Domains', 'Adds risk context for newly registered domains.']] }, 1587 { ic: 'files', t: 'File, Script & Document Analysis', d: 'Inspect file content, scripts and documents.', 1588 f: [['Static File Machine Learning', 'Unknown and zero-day file analysis.'], ['PowerShell Command & Context Analysis', 'Command analysis, context, relationships, and patterns.'], 1589 ['Malicious Script Analysis', 'Malicious VBScript, JScript, WSH and in-memory script content.'], ['Malicious Document Analysis', 'Office, PDF, macro, and embedded content analysis.'], 1590 ['Script Content Inspection', 'Script content via Windows AMSI or the agentâs own scanning.']] }, 1591 { ic: 'behav', t: 'Endpoint Behavior & System Integrity', d: 'Monitor behavior, persistence and privilege activity.', 1592 f: [['Persistence & Autorun Monitoring', 'Services, tasks, run keys, startup items, and WMI subscriptions.'], ['Credential & Privilege Activity', 'Sensitive process, token, account, and privilege activity.'], 1593 ['Security Controls & System Integrity', 'Attempts to weaken security controls, logging, telemetry, updates, or boot security.'], 1594 ['Agent Tamper Detection', 'Attempts to stop, modify, remove, or disable the agent.'], ['Enhanced Process Visibility', 'Detailed process activity for rules and specialized detections.']] }, 1595 { ic: 'net', t: 'Network & Remote Activity', d: 'C2, remote access, lateral movement and exfiltration.', 1596 f: [['Network Behavior & C2 Analytics', 'Beaconing, retries, reverse shells, and rare destinations.'], ['Remote Access & Lateral Movement', 'RDP, SSH, SMB, WinRM, sessions, and remote services.'], 1597 ['Collection & Exfiltration Signals', 'Transfer, staging, USB, and destination correlation.'], ['QUIC & HTTP/3 Traffic Inspection', 'Domains in QUIC and HTTP/3 connections, checked against malicious destinations.']] }, 1598 { ic: 'exploit', t: 'Exploit & Impact Protection', d: 'Block exploits, ransomware and memory threats.', 1599 f: [['Exploit Prevention', 'Application exploitation and exploit-chain interruption.'], ['Process Injection & Memory Threats', 'Injection, hollowing, shellcode, and executable memory.'], 1600 ['Kernel & Driver Abuse', 'Vulnerable driver and kernel-abuse signals.'], ['Anti-Ransomware', 'Canary files, rapid I/O behavior, and immediate containment.'], 1601 ['Destructive System Activity', 'Wipers, backup deletion, and destructive configuration changes.']] }, 1602 { ic: 'rules', t: 'Custom Detection Rules', d: 'Add your own Sigma, YARA and regex rules beside the managed detections.', 1603 f: [['Managed Endpoint & Cloud Detections', 'Assigned through endpoint group policy.'], ['Enable Detection Rules', 'Turn assigned rules on or off for each group.']] }]; 1604 const BEN = [['clock', 'Real-time visibility', 'See threats as they happen.', 'red'], ['aim', 'High-fidelity alerts', 'Less noise. More clarity.', 'navy'], 1605 ['team', 'Faster response', 'Take action with confidence.', 'peri'], ['rise', 'A more secure tomorrow', 'Turn insight into impact.', 'blue']]; 1606 1607 const sec = document.createElement('section'); 1608 sec.className = 'ntx'; sec.id = 'ntx'; 1609 sec.innerHTML = '<div class="ntx-in">' + 1610 '<div class="ntx-head"><p class="ntx-kick">
1610Detect<i></i>Prevent<i></i>Respond</p><span class="ntx-rule"></span><h2>Neutralize Threats <span>in Real Time</span></h2>' + 1611 '<p>Six detection engines on the agent inspect every file, script, process and connection as it happens. What reaches your team is one prioritized alert per attack.</p></div>' + 1612 '<div class="ntx-eng">' + ENG.map(function (e) { 1613 return '<article class="ntx-c"><span class="ntx-hex">' + svg(IC[e.ic]) + '</span><h3>' + e.t + '</h3><p class="ntx-cd">' + e.d + '</p></article>'; 1614 }).join('') + '</div>' + 1615 '<p class="ntx-more"><a href="/threatresponder-platform/#engines">See every engine feature' + svg(IC.arrow) + '</a></p>' + 1616 '<ul class="ntx-ben">' + BEN.map(function (b) { return '<li><span class="t-' + b[3] + '">' + svg(IC[b[0]]) + '</span><b>' + b[1] + '</b><small>' + b[2] + '</small></li>'; }).join('') + '</ul></div>'; 1617 const after = document.getElementById('pdx') || document.getElementById('shx') || document.getElementById('pf') || hero; 1618 after.parentNode.insertBefore(sec, after.nextSibling); 1619 1620 const nextSec = function (from) { let n = from.nextElementSibling; while (n && (n.tagName !== 'SECTION' || getComputedStyle(n).display === 'none')) n = n.nextElementSibling; return n; }; 1621 1622 /* ---- tone: pair with the section before, or alternate; the next section takes the other tone ---- */ 1623 function tone() { 1624 const heroDark = root.getAttribute('data-tone') !== 'light'; 1625 const pd = document.getElementById('pdx'); 1626 /* the section before is Predict when it is on (it takes the opposite tone of the Screens) */ 1627 const prevDark = pd && getComputedStyle(pd).display !== 'none' ? !pd.classList.contains('lt') : document.getElementById('shx') ? heroDark : document.getElementById('pf') ? !heroDark : heroDark; 1628 const dark = root.getAttribute('data-engines') === 'flip' ? !prevDark : prevDark; 1629 sec.classList.toggle('lt', !dark); 1630 [].forEach.call(document.querySelectorAll('.ntx-dk'), function (e) { e.classList.remove('ntx-dk', 'on-dark'); }); 1631 if (root.getAttribute('data-engines') === 'off') return; 1632 /* the two (still light) sections that follow keep alternating: with the Yardstick and the old Neutralize 1633 gone, two lavender sections would otherwise meet */ 1634 const n1 = nextSec(sec), n2 = n1 && nextSec(n1), navy = dark ? n2 : n1; 1635 if (navy && navy.classList.contains('lav') && !navy.classList.contains('on-dark')) navy.classList.add('ntx-dk', 'on-dark'); 1636 } 1637 tone(); 1638 new MutationObserver(tone).observe(root, { attributes: true, attributeFilter: ['data-tone', 'data-engines', 'data-predict'] }); 1639 })(); 1640 1641 /* One combined forensics chapter, following the supplied visual reference. */ 1642 window.renderHomepageForensics(); 1643 1644 /* Reference-led MSSP integrations workspace. */ 1645 window.renderHomepageMssp(); 1646 1647 /* ---- Why Organizations Choose ThreatResponder®, after MSSPs + Integrations. The live site's seven reasons and their 1648 text (netsecurity.com, 24 Sep 2026), but all visible: production hides each one behind a + and leaves the seventh 1649 alone on its row. Save Money leads with its four points (no proof strip: the user noted it is not about saving money 1650 and the hero already says since 2004); the other six sit beside it, 2 à 3. 1651 Replaces the legacy Pinpoint, Extend and Why sections. Opposite tone of the section before. */ 1652 (function () { 1653 const root = document.documentElement; 1654 const hero = document.getElementById('hero'); 1655 if (!hero) return; 1656 const svg = function (p) { return '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' + p + '</svg>'; }; 1657 const IC = { 1658 coin: '<circle cx="12" cy="12" r="9"/><path d="M15 9.5c-.6-1-1.7-1.5-3-1.5-1.7 0-3 .9-3 2.2 0 3 6 1.6 6 4.4 0 1.3-1.3 2.4-3 2.4-1.4 0-2.6-.6-3.2-1.6M12 6v2m0 8v2"/>', 1659 eye: '<path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/>', 1660 lock: '<rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/>', 1661 bolt: '<path d="M13 2 4 14h7l-1 8 9-12h-7z"/>', 1662 doc: '<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6M9 14l2 2 4-4"/>', 1663 chart: '<path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/>', 1664 star: '<path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1 6.2L12 17.3 6.5 20.2l1-6.2L3 9.6l6.2-.9z"/>', 1665 check: '<path d="m5 12 5 5 9-10"/>' 1666 }; 1667 const SAVE = ['Prevent costly cyber attacks and data breaches', 'Eliminate ineffective technologies and gain high ROI', 'Reduce or eliminate the cost of investigations', 'Significantly reduce the cost of security operations']; 1668 const WHY = [['eye', 'Gain Situational Awareness and Quickly Make Informed Decisions', 'You gain threat visibility and pinpoint adversarial activities, so you can quickly make informed decisions to prioritize and mitigate enterprise risks.'], 1669 ['lock', 'Protect Your Intellectual Property', 'Avoid business and operational disruption, protect crucial assets and maintain a competitive and strategic advantage.'], 1670 ['bolt', 'Boost Efficiency and Productivity', 'Stop users from violating policy and enforce strict security policies at the endpoint, with granular controls that keep your security team and staff productive.'], 1671 ['doc', 'Stay Compliant and Avoid Fines', 'Not complying could mean steep fines and penalties. Protect your crucial assets and maintain regulatory compliance.', ['FISMA', 'PCI', 'GDPR', 'SOX', 'HIPAA']], 1672 ['chart', 'Improve Shareholdersâ Value', 'ThreatResponder as your security arsenal eliminates unnecessary security operations costs, increasing your shareholdersâ value.'], 1673 ['star', 'Preserve Your Reputation and Image', 'Stop a successful compromise before it becomes unwanted media coverage, and maintain your business reputation.']]; 1674 const sec = document.createElement('section'); 1675 sec.className = 'wyx'; sec.id = 'wyx'; 1676 sec.innerHTML = '<div class="wyx-in"><div class="wyx-head"><p class="wyx-kick">Business outcomes</p><h2>Why Organizations Choose ThreatResponder®</h2></div><div class="wyx-grid">' + 1677 '<article class="wyx-save"><span class="wyx-ic">' + svg(IC.coin) + '</span><h3>Save Money</h3><ul>' + SAVE.map(function (s) { return '<li><i>' + svg(IC.check) + '</i>' + s + '</li>'; }).join('') + '</ul>' + '</article>' + 1678 WHY.map(function (w, i) { 1679 return '<article class="wyx-c" style="--i:' + i + '"><span class="wyx-ic">' + svg(IC[w[0]]) + '</span><div><h3>' + w[1] + '</h3><p>' + w[2] + '</p>' + (w[3] ? '<p class="wyx-reg">' + w[3].map(function (r) { return '<i>' + r + '</i>'; }).join('') + '</p>' : '') + '</div></article>'; 1680 }).join('') + '</div></div>'; 1681 const after = document.getElementById('igx') || document.getElementById('tbx') || document.getElementById('fsx') || hero; 1682 after.parentNode.insertBefore(sec, after.nextSibling); 1683 1684 /* ---- tone: the opposite of the section before ---- */ 1685 const vis = function (el) { return el && getComputedStyle(el).display !== 'none'; }; 1686 const darkOf = function (p) { 1687 if (!p) return true; 1688 if (/\b(ntx|pdx|fsx|tbx|igx|wyx|tmx)\b/.test(p.className)) return !p.classList.contains('lt'); 1689 if (p.id === 'shx') return root.getAttribute('data-tone') !== 'light'; 1690 return p.classList.contains('on-dark'); 1691 }; 1692 function tone() { 1693 let p = sec.previousElementSibling; 1694 while (p && (p.tagName !== 'SECTION' || !vis(p))) p = p.previousElementSibling; 1695 sec.classList.toggle('lt', darkOf(p)); 1696 } 1697 tone(); 1698 /* after the integrations section (20 ms) has re-toned for the same change */ 1699 new MutationObserver(function () { setTimeout(tone, 30); }).observe(root, { attributes: true, attributeFilter: ['data-tone', 'data-engines', 'data-predict'] }); 1700 })(); 1701 /* ---- Testimonials, right after Why choose. The live site's seven unique quotes, word for word (netsecurity.com, 1702 24 Sep 2026; production loops them in a one-at-a-time carousel). Only two are about ThreatResponder: they are the 1703 two large cards. The five from security services work (audits, penetration tests, an engagement) drift past below 1704 in a slow ribbon (in view only, pauses on hover; still under reduced motion), each labelled with its service. 1705 Replaces the legacy Trusted-by and Testimonials sections (the hero already carries the client logos). */ 1706 (function () { 1707 const root = document.documentElement; 1708 const hero = document.getElementById('hero'); 1709 if (!hero) return; 1710 const still = root.classList.contains('poster') || root.classList.contains('reduced'); 1711 const PRODUCT = [['ThreatResponder is a very innovative and essential cybersecurity platform. By deploying the ThreatResponder Platform, our team can detect and prevent advanced cyber attacks that existing security products were not able to detect. As a result, we have eliminated ineffective technologies and now gain situational awareness of all our technology assets. With ThreatResponder, we are better positioned to prioritize and mitigate enterprise risks timely and cost-effectively.', 'Jim Brundage', 'VP Operations, CKA', 'JB'], 1712 ['NetSecurityâs ThreatResponder Platform provided comprehensive services in detecting, responding, and preventing serious and prevalent cyber attacks. It allows us to focus on our core business in delivering infrastru
1712cture and digital transformation services and building confidence with our clients.', 'Thuan Nguyen', 'CEO, Whonome, LTD', 'TN']]; 1713 const SERVICES = [['Security audit', 'The security audit we received from NetSecurity was integral to the planning and design of our suite of web based service products⦠This is sensitive data and protected by law. NetSecurity helped us make our web products both secure and compliant with FERPA requirements.', 'Director of Services, Ellucian'], 1714 ['Penetration testing', 'Thank you for an outstanding job on the penetration testing! I am very impressed by your knowledge and professionalism. I look forward to working with you on future engagements.', 'Blake Turrentine, Founder & Chief CTO, HotWan'], 1715 ['Security audit', 'Excellent job on the security audit performed by NetSecurity. It provided an in-depth view of our network that offers search and registration services. NetSecurity was able to demonstrate their knowledge on latest network security issues.', 'Thuan Nguyen, Partner, Whonome, Limited'], 1716 ['Application security testing', 'Thank you very much for all the hard work, focus, expertise, and time you have put into testing the security of our web-based payroll application⦠All that we have learned so far will become part of our standard development procedures.', 'Lead Software Developer, a Major International Payroll Company'], 1717 ['Engagement support', 'NetSecurityâs ability to devote an immense effort, with little to no advanced notice, and deliver a work product that really shines is something that we have come to rely on here at our firm.', 'President & CEO, System Integrator']]; 1718 const q = '<svg class="tmx-q" viewBox="0 0 40 32" aria-hidden="true"><path d="M0 32V19C0 8 6 1.5 16 0l1.5 4.5C11.5 6.5 9 10.5 9 15h7v17zm23 0V19c0-11 6-17.5 16-19l1.5 4.5C34.5 6.5 32 10.5 32 15h7v17z"/></svg>'; 1719 const svc = function (s) { return '<figure class="tmx-s"><small>' + s[0] + '</small><blockquote>â' + s[1] + 'â</blockquote><figcaption>' + s[2] + '</figcaption></figure>'; }; 1720 const sec = document.createElement('section'); 1721 sec.className = 'tmx' + (still ? ' still' : ''); sec.id = 'tmx'; 1722 sec.innerHTML = '<div class="tmx-in"><div class="tmx-head"><p class="tmx-kick">Testimonials</p><h2>In Their Words</h2></div>' + 1723 '<div class="tmx-prod">' + PRODUCT.map(function (p) { 1724 return '<figure class="tmx-p">' + q + '<span class="tmx-tag">ThreatResponder® Platform</span><blockquote>' + p[0] + '</blockquote><figcaption><i>' + p[3] + '</i><span><b>' + p[1] + '</b><small>' + p[2] + '</small></span></figcaption></figure>'; 1725 }).join('') + '</div>' + 1726 '<p class="tmx-sub">From our security services work</p><div class="tmx-rib"><div class="tmx-track">' + SERVICES.map(svc).join('') + SERVICES.map(svc).join('').replace(/<figure class="tmx-s">/g, '<figure class="tmx-s" aria-hidden="true">') + '</div></div></div>'; 1727 const after = document.getElementById('wyx') || document.getElementById('igx') || document.getElementById('tbx') || hero; 1728 after.parentNode.insertBefore(sec, after.nextSibling); 1729 1730 /* ---- tone: the opposite of Why choose. The earlier sections' scripts run before these two sections exist and may 1731 have drawn Get started navy as their next section; that is undone here (Get started keeps its own lavender) ---- */ 1732 const vis = function (el) { return el && getComputedStyle(el).display !== 'none'; }; 1733 const darkOf = function (p) { 1734 if (!p) return true; 1735 if (/\b(ntx|pdx|fsx|tbx|igx|wyx|tmx)\b/.test(p.className)) return !p.classList.contains('lt'); 1736 if (p.id === 'shx') return root.getAttribute('data-tone') !== 'light'; 1737 return p.classList.contains('on-dark'); 1738 }; 1739 function tone() { 1740 let p = sec.previousElementSibling; 1741 while (p && (p.tagName !== 'SECTION' || !vis(p))) p = p.previousElementSibling; 1742 sec.classList.toggle('lt', darkOf(p)); 1743 [].forEach.call(document.querySelectorAll('.wyx ~ .sec[class*="-dk"]'), function (e) { e.classList.remove('ntx-dk', 'fsx-dk', 'tbx-dk', 'igx-dk', 'on-dark'); }); 1744 } 1745 tone(); 1746 new MutationObserver(function () { setTimeout(tone, 40); }).observe(root, { attributes: true, attributeFilter: ['data-tone', 'data-engines', 'data-predict'] }); 1747 1748 /* the ribbon moves only while on screen */ 1749 if (still) return; 1750 new IntersectionObserver(function (en) { sec.classList.toggle('run', en[0].isIntersecting); }, { threshold: 0 }).observe(sec.querySelector('.tmx-rib')); 1751 })(); 1752 /* ---- Get started, About and the footer (static markup from outro.html.txt, same content as before). Get started takes 1753 the opposite tone of Testimonials and About the opposite of Get started; the footer stays the deepest navy. The 1754 Live demo / 15-day trial switch relabels the submit button. */ 1755 (function () { 1756 const root = document.documentElement; 1757 const gs = document.getElementById('started'), ab = document.querySelector('.abx'); 1758 if (!gs || !ab) return; 1759 const vis = function (el) { return el && getComputedStyle(el).display !== 'none'; }; 1760 const darkOf = function (p) { 1761 if (!p) return true; 1762 if (/\b(ntx|pdx|fsx|tbx|igx|wyx|tmx|gsx|abx)\b/.test(p.className)) return !p.classList.contains('lt'); 1763 if (p.id === 'shx') return root.getAttribute('data-tone') !== 'light'; 1764 return p.classList.contains('on-dark'); 1765 }; 1766 const prev = function (el) { let p = el.previousElementSibling; while (p && (p.tagName !== 'SECTION' || !vis(p))) p = p.previousElementSibling; return p; }; 1767 function tone() { 1768 gs.classList.toggle('lt', darkOf(prev(gs))); 1769 ab.classList.toggle('lt', darkOf(gs)); 1770 } 1771 tone(); 1772 /* after Testimonials (40 ms) has re-toned for the same change */ 1773 new MutationObserver(function () { setTimeout(tone, 50); }).observe(root, { attributes: true, attributeFilter: ['data-tone', 'data-engines', 'data-predict'] }); 1774 1775 const seg = [].slice.call(gs.querySelectorAll('.gsx-seg button')), go = gs.querySelector('.gsx-btn'); 1776 seg.forEach(function (b) { 1777 b.addEventListener('click', function () { 1778 seg.forEach(function (x) { x.classList.toggle('on', x === b); x.setAttribute('aria-selected', x === b ? 'true' : 'false'); }); 1779 go.textContent = b.getAttribute('data-cta'); 1780 }); 1781 }); 1782 })(); 1783 /* ---- Product tour: "Take the product tour" (hero) and "Product tour" (menus, footer) open the console itself in a 1784 window over the page: the console mockup (/mockup/) with its full header and side menu, on sample data. The 1785 tooltips are the product's own tour engine (driver.js, the ThreatResponder build in helpers/driver) configured as 1786 ui/src/services/featureTour.service.js does: one step at a time, Next / Back / Skip Tour, "Step n of N", arrow keys. 1787 Phase 1 covers the Dashboard only: every other menu item, header control and module is shown disabled (greyed out, 1788 a not-allowed cursor, no response), and anything else that would leave the Dashboard is announced as coming to the 1789 tour. #tour in the URL opens it on load. */ 1790 (function () { 1791 const root = document.documentElement; 1792 const APP = '/mockup/', PAGE = 'dashboard.html'; 1793 const theme = function () { return root.getAttribute('data-tone') === 'light' ? 'light' : 'dark'; }; 1794 const x = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>'; 1795 const re = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg>'; 1796 const box = document.createElement('div'); 1797 box.className = 'ptx'; box.hidden = true; 1798 box.setAttribute('role', 'dialog'); box.setAttribute('aria-modal', 'true'); box.setAttribute('aria-label', 'ThreatResponder product tour'); 1799 box.innerHTML = '<div class="ptx-win"><div class="ptx-bar"><i></i><i></i><i></i><span class="ptx-url">console.threatresponder.com / <em>dashboard</em></span>' + 1800 '<nav class="ptx-mods" aria-label="Tour modules"></nav><span class="ptx-sp"></span><span class="ptx-tag">Sample data</span>' + 1801 '<button type="button" class="ptx-re">' + re + 'Restart tour</button><a class="ptx-demo cta" href="#started">Book a free demo</a>' + 1802 '<button type="button" class="ptx-x" aria-label="Close the product tour">' + x + '</button></div>' + 1803 '<div class="ptx-stage"><iframe title="ThreatResponder console, product tour"></iframe><p class="ptx-load"><i></i>
1803Opening the consoleâ¦</p></div>' + 1804 '<p class="ptx-toast" role="status" aria-live="polite"></p></div>'; 1805 document.body.appendChild(box); 1806 const mods = box.querySelector('.ptx-mods'); 1807 const stage = box.querySelector('.ptx-stage'), frame = box.querySelector('iframe'), toastEl = box.querySelector('.ptx-toast'); 1808 let win = null, doc = null, drv = null, cur = 0, ready = false, wantTour = false, back = null, toastT = 0; 1809 1810 /* the console lays out at 1280 px at least; a narrower window scales it down */ 1811 const MIN_W = 1280; 1812 function fit() { 1813 const w = stage.clientWidth, h = stage.clientHeight; 1814 if (!w) return; 1815 const k = Math.min(1, w / MIN_W); 1816 frame.style.width = (w / k) + 'px'; frame.style.height = (h / k) + 'px'; frame.style.transform = k < 1 ? 'scale(' + k.toFixed(4) + ')' : ''; 1817 } 1818 if (window.ResizeObserver) new ResizeObserver(fit).observe(stage); 1819 1820 function toast(msg) { 1821 toastEl.innerHTML = msg; toastEl.classList.add('on'); 1822 clearTimeout(toastT); toastT = setTimeout(function () { toastEl.classList.remove('on'); }, 3600); 1823 } 1824 1825 /* ---- the steps: what is on screen, in the product's words ---- */ 1826 const panel = function (pane, title) { 1827 return function () { 1828 const h = [].slice.call(doc.querySelectorAll('#dbxPane-' + pane + ' .dbx-panel-header')).find(function (e) { return e.textContent.trim().indexOf(title) === 0; }); 1829 return h ? h.closest('.dbx-panel') : null; 1830 }; 1831 }; 1832 const tab = function (name) { 1833 return function () { 1834 const b = [].slice.call(doc.querySelectorAll('.dbx-tab-btn')).find(function (e) { return e.textContent.trim() === name; }); 1835 win.scrollTo(0, 0); 1836 if (b && !b.classList.contains('active')) { b.click(); return 700; } 1837 return 0; 1838 }; 1839 }; 1840 /* module by module, not a user guide (user, 24 Sep): each module is a chapter shown on its own page; its steps 1841 say what the module is for. Phase 1 has the Dashboard; Alerts, Live View, Forensics and Hunt follow as chapters. */ 1842 const MODULES = [ 1843 { name: 'Dashboard', page: 'dashboard.html', steps: [ 1844 { before: tab('Overview'), page: true, title: 'Dashboard · Overview', text: 'The whole estate at a glance, for one company or all of them: security posture, threats, vulnerabilities, identity, endpoints and network, each with its trend and what needs action.' }, 1845 { before: tab('Threats'), page: true, title: 'Dashboard · Threats', text: 'Detections over time and by engine, how fast they are resolved, and how many were auto-resolved with no analyst action needed.' }, 1846 { before: tab('Endpoints'), page: true, title: 'Dashboard · Endpoints', text: 'Vital signs for every endpoint: the failing checks ranked by how many endpoints they hit, and the endpoints that need attention first.' }, 1847 { before: tab('Network'), page: true, title: 'Dashboard · Network', text: 'Where data leaves the fleet: egress over time and by destination country, then risk signals such as single uploads large enough to look like exfiltration.' }, 1848 { before: tab('ITDR'), page: true, title: 'Dashboard · Identity (ITDR)', text: 'Failed logons and lockouts per day, a risk score for every identity, and identity detections such as repeated failed logons across many accounts.' }, 1849 { before: tab('Exposure'), page: true, title: 'Dashboard · Exposure', text: 'Vulnerabilities ranked by real risk: actively exploited first, then critical and high, with patching progress over the last 7 days.' } 1850 ] }, 1851 { name: 'Alerts', soon: true }, { name: 'Live View', soon: true }, { name: 'Forensics', soon: true }, { name: 'Hunt', soon: true } 1852 ]; 1853 const STEPS = []; 1854 mods.innerHTML = MODULES.map(function (m, k) { return '<button type="button"' + (m.soon ? ' class="soon" disabled title="Coming to the tour soon"' : '') + ' data-m="' + k + '">' + m.name + (m.soon ? '<small>soon</small>' : '') + '</button>'; }).join(''); 1855 MODULES.forEach(function (m) { if (m.steps) m.steps.forEach(function (st, j) { st.mod = m; st.n = j; STEPS.push(st); }); }); 1856 1857 /* ---- the runner (featureTour.service.js: a fresh driver per step) ---- */ 1858 function center() { 1859 let v = doc.querySelector('.feature-tour-virtual-element'); 1860 if (!v) { v = doc.createElement('div'); v.className = 'feature-tour-virtual-element'; doc.body.appendChild(v); } 1861 return v; 1862 } 1863 /* driver.js puts position: relative on the highlighted element, which unpins a fixed menu or header part: the 1864 element keeps its own position while it is highlighted */ 1865 let pinned = null; 1866 function keep(el) { 1867 const p = win.getComputedStyle(el).position; 1868 if (p === 'fixed' || p === 'absolute' || p === 'sticky') { pinned = [el, el.style.position]; el.style.position = p; } 1869 } 1870 function stop() { 1871 if (drv) { try { drv.destroy(); } catch (e) { /* already gone */ } drv = null; } 1872 if (pinned) { pinned[0].style.position = pinned[1]; pinned = null; } 1873 } 1874 /* the welcome and closing cards sit in the middle of the console, without an arrow */ 1875 function middle() { 1876 const p = doc.querySelector('.driver-popover'); 1877 if (!p) return; 1878 p.style.left = Math.round((win.innerWidth - p.offsetWidth) / 2) + 'px'; 1879 p.style.top = Math.round((win.innerHeight - p.offsetHeight) * .42) + 'px'; 1880 } 1881 function show(i) { 1882 stop(); 1883 if (!ready || i < 0 || i >= STEPS.length) return; 1884 cur = i; 1885 const s = STEPS[i], wait = s.before ? s.before() : 0; 1886 markMod(s.mod); 1887 setTimeout(function () { 1888 const el = s.center || s.page ? center() : typeof s.el === 'function' ? s.el() : doc.querySelector(s.el); 1889 const last = i === STEPS.length - 1; 1890 if (el) keep(el); 1891 drv = win.driver({ 1892 animate: true, overlayColor: s.page ? 'rgba(0, 0, 0, 0)' : 'rgba(0, 0, 0, 0.7)', stagePadding: s.center || s.page ? 0 : 10, stageRadius: 8, allowClose: true, 1893 doneBtnText: last ? 'Finish Tour' : 'Next â', nextBtnText: 'Next â', prevBtnText: 'â Back', skipBtnText: 'Skip Tour', 1894 showProgress: true, progressText: s.mod.name + ' · ' + (s.n + 1) + ' of ' + s.mod.steps.length, popoverClass: 'feature-tour-popover ptx-pop' + (s.center ? ' ptx-
1894pop--center' : '') + (s.page ? ' ptx-pop--dock' : ''), 1895 steps: [{ element: el || 'body', popover: { title: s.title, description: s.text, side: s.center ? 'bottom' : s.side, align: s.center ? 'center' : s.align || 'center' } }], 1896 onNextClick: function () { if (last) end(); else show(i + 1); }, 1897 onPrevClick: function () { if (i > 0) show(i - 1); }, 1898 onCloseClick: end, onSkipClick: end, onDestroyed: function () { drv = null; } 1899 }); 1900 drv.drive ? drv.drive() : drv.highlight && drv.highlight(); 1901 setTimeout(function () { const p = doc.querySelector('.driver-popover-prev-btn'); if (p) p.style.display = i === 0 ? 'none' : ''; if (s.center) middle(); }, 0); 1902 }, wait); 1903 } 1904 function markMod(m) { [].forEach.call(mods.children, function (b) { b.classList.toggle('on', b.textContent.indexOf(m.name) === 0); }); } 1905 function end() { 1906 stop(); 1907 if (cur === STEPS.length - 1) toast('Next in the tour: <b>Alerts</b>, <b>Live View</b>, <b>Forensics</b> and <b>Hunt</b>. For now, explore the Dashboard.'); 1908 const b = [].slice.call(doc.querySelectorAll('.dbx-tab-btn')).find(function (e) { return e.textContent.trim() === 'Overview'; }); 1909 if (b && !b.classList.contains('active')) b.click(); 1910 } 1911 1912 /* ---- the console: loaded once, on first open; only the Dashboard is part of the tour so far ---- */ 1913 function label(a) { return (a.getAttribute('title') || a.textContent).trim().replace(/\s+/g, ' ').replace(/\s*\d+$/, '').slice(0, 40); } 1914 /* everything in the console's menus except the Dashboard; CSS selectors, so rows the side menu rebuilds stay disabled */ 1915 const OFF = '#nvSide .nv-row:not([href="dashboard.html"]),#nvSide .nv-pin,#nvSide .nv-search,.page-header .top-menu .dropdown-toggle,.page-header .unified-logout-btn,.global-company-context,.unified-user-section,.ovc-btn'; 1916 function soon(what) { toast('<b>' + (what || 'That page') + '</b> is coming to the tour soon. For now, explore the Dashboard.'); } 1917 function prep() { 1918 doc = frame.contentDocument; win = frame.contentWindow; 1919 doc.documentElement.setAttribute('data-theme', theme()); 1920 const st = doc.createElement('style'); 1921 /* the mockup's Network tab lets its uploads table spill past the page (2009 px wide at 1920): the table scrolls in its 1922 panel instead, and the page never scrolls sideways (driver.js centres targets horizontally too) */ 1923 st.textContent = '#ovxDock,.ovx-dock,.nv-sec--lab{display:none!important}html{overflow-x:clip}#dbxPane-network .dbx-panel-body--flush{overflow-x:auto}' + 1924 '.feature-tour-virtual-element{position:fixed;top:36%;left:50%;width:1px;height:1px;opacity:0;pointer-events:none}' + 1925 '.driver-popover.feature-tour-popover{max-width:360px}.feature-tour-popover .driver-popover-title{font-size:17px;margin-bottom:10px}' + 1926 OFF.split(',').map(function (q) { return q + '{opacity:.3!important;filter:grayscale(1);cursor:not-allowed!important}'; }).join('') + 1927 OFF.split(',').map(function (q) { return q + ':hover{background:none!important}'; }).join('') + 1928 '.ptx-side-note{margin:10px 12px 6px;padding:9px 11px;border-radius:8px;background:rgba(47,111,237,.14);color:#9fb6ec;font-size:11.5px;line-height:1.45}.ptx-side-note b{color:#dbe6ff}[data-theme=light] .ptx-side-note{background:#e8efff;color:#3d5a99}[data-theme=light] .ptx-side-note b{color:#1c3f8f}' + 1929 '.feature-tour-popover .driver-popover-description{font-size:14px;line-height:1.7}.ptx-pop .driver-popover-arrow{display:none}.driver-popover.ptx-pop--dock{left:auto!important;top:auto!important;right:28px!important;bottom:28px!important}.ptx-pop--dock{box-shadow:0 0 0 1px rgba(110,160,255,.45),0 24px 60px rgba(0,0,0,.45)!important}'; 1930 doc.head.appendChild(st); 1931 const side = doc.getElementById('nvSide'), note = doc.createElement('p'); 1932 note.className = 'ptx-side-note'; note.innerHTML = '<b>Product tour:</b> the Dashboard. More modules are coming soon.'; 1933 if (side) side.insertBefore(note, side.firstChild); 1934 const css = doc.createElement('link'); css.rel = 'stylesheet'; css.href = 'helpers/driver/driver.min.css'; doc.head.appendChild(css); 1935 const js = doc.createElement('script'); js.src = 'helpers/driver/driver.min.js'; 1936 js.onload = function () { ready = true; box.classList.add('ok'); if (wantTour) { wantTour = false; show(0); } }; 1937 doc.head.appendChild(js); 1938 /* links to other pages stay on the Dashboard and say what is coming */ 1939 doc.addEventListener('click', function (e) { 1940 if (e.target.closest && e.target.closest(OFF)) { e.preventDefault(); e.stopPropagation(); return; } 1941 const a = e.target.closest && e.target.closest('a[href]'); 1942 if (a) { 1943 const h = a.getAttribute('href'); 1944 if (/\.html(\?|#|$)/.test(h) && h.indexOf(PAGE) !== 0) { e.preventDefault(); e.stopPropagation(); soon(label(a)); } 1945 return; 1946 } 1947 if (e.target.closest && e.target.closest('.unified-logout-btn')) { e.preventDefault(); e.stopPropagation(); toast('This is a demo console: there is nothing to sign out of.'); } 1948 }, true); 1949 /* keys: this driver build ends a one-step tour on ArrowRight, so the tour's own keys are taken first */ 1950 doc.addEventListener('keydown', keys, true); 1951 doc.addEventListener('focusin', function (e) { if (e.target.closest && e.target.closest(OFF)) e.target.blur(); }); 1952 [].forEach.call(doc.querySelectorAll(OFF), function (el) { el.setAttribute('tabindex', '-1'); el.setAttribute('aria-disabled', 'true'); }); 1953 } 1954 frame.addEventListener('load', function () { 1955 let d = null;
1956 try { d = frame.contentDocument; } catch (e) { return; } 1957 if (!d) return; 1958 /* anything that still navigated away (a script link) comes back to the Dashboard */ 1959 if (d.URL.indexOf(PAGE) < 0) { ready = false; box.classList.remove('ok'); soon(''); frame.src = APP + PAGE + '?theme=' + theme(); return; } 1960 const t0 = Date.now(); 1961 (function wait() { 1962 if (d.querySelector('.ovx-tiles') && d.querySelector('#nvSide')) { prep(); return; } 1963 if (Date.now() - t0 < 10000) setTimeout(wait, 80); 1964 })(); 1965 }); 1966 1967 function open(push) { 1968 if (!box.hidden) return; 1969 back = document.activeElement; 1970 box.hidden = false; root.classList.add('ptx-open'); 1971 requestAnimationFrame(function () { box.classList.add('in'); fit(); }); 1972 if (!frame.getAttribute('src')) { wantTour = true; frame.src = APP + PAGE + '?theme=' + theme(); } 1973 else { 1974 if (doc) doc.documentElement.setAttribute('data-theme', theme()); 1975 if (ready) show(0); else wantTour = true; 1976 } 1977 if (push && location.hash !== '#tour') history.replaceState(null, '', location.pathname + location.search + '#tour'); 1978 box.querySelector('.ptx-x').focus(); 1979 } 1980 function close() { 1981 if (box.hidden) return; 1982 stop(); box.classList.remove('in'); root.classList.remove('ptx-open'); 1983 setTimeout(function () { box.hidden = true; }, 220); 1984 if (location.hash === '#tour') history.replaceState(null, '', location.pathname + location.search); 1985 if (back && back.focus) back.focus({ preventScroll: true }); 1986 } 1987 box.querySelector('.ptx-x').addEventListener('click', close); 1988 box.querySelector('.ptx-re').addEventListener('click', function () { if (ready) show(0); }); 1989 mods.addEventListener('click', function (e) { 1990 const b = e.target.closest('button'); if (!b) return; 1991 const m = MODULES[+b.getAttribute('data-m')]; 1992 if (m.soon) return; 1993 if (ready) show(STEPS.indexOf(m.steps[0])); 1994 }); 1995 box.querySelector('.ptx-demo').addEventListener('click', function (e) { 1996 e.preventDefault(); close(); 1997 const g = document.getElementById('started'); if (g) setTimeout(function () { g.scrollIntoView({ behavior: 'smooth' }); }, 240); 1998 }); 1999 function keys(e) { 2000 if (box.hidden) return; 2001 const t = e.target, typing = t && (/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName) || t.isContentEditable); 2002 if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); if (drv) end(); else close(); return; } 2003 if (!drv || typing) return; 2004 if (e.key === 'ArrowRight' || (e.key === 'Enter' && !(t && t.closest && t.closest('button,a')))) { e.preventDefault(); e.stopPropagation(); if (cur === STEPS.length - 1) end(); else show(cur + 1); } 2005 else if (e.key === 'ArrowLeft') { e.preventDefault(); e.stopPropagation(); if (cur > 0) show(cur - 1); } 2006 } 2007 document.addEventListener('keydown', keys, true); 2008 /* every "product tour" link on the page, including the header menus built later */ 2009 document.addEventListener('click', function (e) { 2010 const a = e.target.closest && e.target.closest('a'); 2011 if (a && !box.contains(a) && /product tour/i.test(a.textContent)) { e.preventDefault(); open(true); } 2012 }); 2013 if (location.hash === '#tour') setTimeout(function () { open(false); }, 300); 2014 })(); 2015 document.addEventListener('site:ready', function () { 2016 /* the stack builds and the spine draws when they arrive */ 2017 ['stack', 'spine'].forEach(function (id) { var el = document.getElementById(id); if (still) { el.classList.add('in'); return; } new IntersectionObserver(function (es, o) { if (es[0].isIntersecting) { el.classList.add('in'); o.disconnect(); } }, { threshold: .3 }).observe(el); }); 2018 /* hero captions appear once the console has loaded */ 2019 var ov = document.getElementById('hero-ov').__pui, caps = [document.getElementById('cap1'), document.getElementById('cap2')]; 2020 function show() { caps.forEach(function (x, i) { setTimeout(function () { x.classList.add('on'); }, still ? 0 : i * 600); }); } 2021 if (still || ov.static) show(); else ov.onTick(function (p) { if (p >= 1) show(); }); 2022 document.querySelectorAll('form.f .seg span').forEach(function (s, i, all) { s.addEventListener('click', function () { all.forEach(function (x) { x.classList.toggle('on', x === s); }); document.querySelector('form.f button').textContent = i ? 'Start the 15-day trial' : 'Book a free demo'; }); }); 2023 }); 2024})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.