fix(web/dashboard): wrap long kv-chips instead of blowing out the EVENT column

Key:value chips in the live-feed event cell used the default .chip
style, which is white-space: nowrap + inline-flex. A long cmd: value
(attacker-controlled shell strings, URLs, base64 payloads) stretched
the chip horizontally past the column, pushing the whole table into
horizontal scroll and clipping subsequent columns off-screen.

Add a chip-kv variant that allows the value to wrap inside a
max-width: 100% chip (word-break: break-word, overflow-wrap: anywhere
for dense strings with no natural break). The key-label stays on the
first line via flex-shrink: 0. Short values (uid: 0, user: root)
stay tight; long ones wrap onto multiple lines inside the chip.

Also set minWidth: 0 on the EVENT td + nested flex containers so
flex children honour the column width instead of growing to fit
content. Added title={k: v} on each chip for full-value hover in
case the wrap is still clipped.
This commit is contained in:
2026-04-24 00:51:31 -04:00
parent bfff212a05
commit c282f74bd4
2 changed files with 33 additions and 9 deletions

View File

@@ -71,6 +71,22 @@
background: rgba(255, 65, 65, 0.1);
}
/* Key-value chips in the live-feed event column. Values are unbounded
(attacker-supplied command strings, URLs, base64 payloads), so these
must wrap inside the chip rather than inherit the default nowrap —
otherwise a single `cmd: echo <2KB>` blows out the table width. */
.chip.chip-kv {
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
max-width: 100%;
align-items: flex-start;
line-height: 1.35;
}
.chip.chip-kv > .dim {
flex-shrink: 0;
}
/* Breach banner */
.breach-banner {
background: rgba(255, 65, 65, 0.1);

View File

@@ -359,8 +359,8 @@ const Dashboard: React.FC<DashboardProps> = ({ searchQuery }) => {
<td className="violet-accent">{log.decky}</td>
<td><span className="chip dim-chip">{log.service}</span></td>
<td className="matrix-text">{log.attacker_ip}</td>
<td>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<td style={{ minWidth: 0, maxWidth: 0, width: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
<div style={{ fontWeight: 700, fontSize: '0.78rem', color: 'var(--text-color)' }}>
{(() => {
const et = log.event_type && log.event_type !== '-' ? log.event_type : null;
@@ -378,7 +378,7 @@ const Dashboard: React.FC<DashboardProps> = ({ searchQuery }) => {
})()}
</div>
{Object.keys(parsedFields).length > 0 && (
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', minWidth: 0 }}>
{parsedFields.stored_as != null && (
<button
onClick={() => setArtifact({
@@ -404,12 +404,20 @@ const Dashboard: React.FC<DashboardProps> = ({ searchQuery }) => {
)}
{Object.entries(parsedFields)
.filter(([k]) => k !== 'meta_json_b64' && k !== 'stored_as')
.map(([k, v]) => (
<span key={k} className="chip matrix" style={{ fontSize: '0.62rem' }}>
<span className="dim" style={{ marginRight: 3 }}>{k}:</span>
{typeof v === 'object' ? JSON.stringify(v) : String(v)}
</span>
))}
.map(([k, v]) => {
const rendered = typeof v === 'object' ? JSON.stringify(v) : String(v);
return (
<span
key={k}
className="chip matrix chip-kv"
style={{ fontSize: '0.62rem' }}
title={`${k}: ${rendered}`}
>
<span className="dim" style={{ marginRight: 3 }}>{k}:</span>
{rendered}
</span>
);
})}
</div>
)}
</div>