fix: service badges filter commands/fingerprints locally

Clicking a service badge in the attacker detail view now filters the
commands and fingerprints sections on that page instead of navigating
away. Click again to clear. Header shows filtered/total counts.
This commit is contained in:
2026-04-14 01:38:24 -04:00
parent 24e0d98425
commit 8c249f6987

View File

@@ -216,6 +216,7 @@ const AttackerDetail: React.FC = () => {
const [attacker, setAttacker] = useState<AttackerData | null>(null); const [attacker, setAttacker] = useState<AttackerData | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [serviceFilter, setServiceFilter] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchAttacker = async () => { const fetchAttacker = async () => {
@@ -330,17 +331,27 @@ const AttackerDetail: React.FC = () => {
<h2>SERVICES TARGETED</h2> <h2>SERVICES TARGETED</h2>
</div> </div>
<div style={{ padding: '16px', display: 'flex', flexWrap: 'wrap', gap: '8px' }}> <div style={{ padding: '16px', display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{attacker.services.length > 0 ? attacker.services.map((svc) => ( {attacker.services.length > 0 ? attacker.services.map((svc) => {
<span const isActive = serviceFilter === svc;
key={svc} return (
className="service-badge" <span
style={{ fontSize: '0.85rem', padding: '4px 12px', cursor: 'pointer' }} key={svc}
onClick={() => navigate(`/attackers?service=${encodeURIComponent(svc)}`)} className="service-badge"
title={`Filter attackers by ${svc.toUpperCase()}`} style={{
> fontSize: '0.85rem', padding: '4px 12px', cursor: 'pointer',
{svc.toUpperCase()} ...(isActive ? {
</span> backgroundColor: 'var(--text-color)',
)) : ( color: 'var(--bg-color)',
borderColor: 'var(--text-color)',
} : {}),
}}
onClick={() => setServiceFilter(isActive ? null : svc)}
title={isActive ? 'Clear filter' : `Filter by ${svc.toUpperCase()}`}
>
{svc.toUpperCase()}
</span>
);
}) : (
<span className="dim">No services recorded</span> <span className="dim">No services recorded</span>
)} )}
</div> </div>
@@ -371,59 +382,76 @@ const AttackerDetail: React.FC = () => {
</div> </div>
{/* Commands */} {/* Commands */}
<div className="logs-section"> {(() => {
<div className="section-header"> const filteredCmds = serviceFilter
<h2>COMMANDS ({attacker.commands.length})</h2> ? attacker.commands.filter((cmd) => cmd.service === serviceFilter)
</div> : attacker.commands;
{attacker.commands.length > 0 ? ( return (
<div className="logs-table-container"> <div className="logs-section">
<table className="logs-table"> <div className="section-header">
<thead> <h2>COMMANDS ({filteredCmds.length}{serviceFilter ? ` / ${attacker.commands.length}` : ''})</h2>
<tr> </div>
<th>TIMESTAMP</th> {filteredCmds.length > 0 ? (
<th>SERVICE</th> <div className="logs-table-container">
<th>DECKY</th> <table className="logs-table">
<th>COMMAND</th> <thead>
</tr> <tr>
</thead> <th>TIMESTAMP</th>
<tbody> <th>SERVICE</th>
{attacker.commands.map((cmd, i) => ( <th>DECKY</th>
<tr key={i}> <th>COMMAND</th>
<td className="dim" style={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}> </tr>
{cmd.timestamp ? new Date(cmd.timestamp).toLocaleString() : '-'} </thead>
</td> <tbody>
<td>{cmd.service}</td> {filteredCmds.map((cmd, i) => (
<td className="violet-accent">{cmd.decky}</td> <tr key={i}>
<td className="matrix-text" style={{ fontFamily: 'monospace' }}>{cmd.command}</td> <td className="dim" style={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}>
</tr> {cmd.timestamp ? new Date(cmd.timestamp).toLocaleString() : '-'}
))} </td>
</tbody> <td>{cmd.service}</td>
</table> <td className="violet-accent">{cmd.decky}</td>
<td className="matrix-text" style={{ fontFamily: 'monospace' }}>{cmd.command}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}>
{serviceFilter ? `NO ${serviceFilter.toUpperCase()} COMMANDS CAPTURED` : 'NO COMMANDS CAPTURED'}
</div>
)}
</div> </div>
) : ( );
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}> })()}
NO COMMANDS CAPTURED
</div>
)}
</div>
{/* Fingerprints */} {/* Fingerprints */}
<div className="logs-section"> {(() => {
<div className="section-header"> const filteredFps = serviceFilter
<h2>FINGERPRINTS ({attacker.fingerprints.length})</h2> ? attacker.fingerprints.filter((fp) => {
</div> const p = getPayload(fp);
{attacker.fingerprints.length > 0 ? ( return p.service === serviceFilter;
<div style={{ padding: '16px', display: 'flex', flexDirection: 'column', gap: '12px' }}> })
{attacker.fingerprints.map((fp, i) => ( : attacker.fingerprints;
<FingerprintCard key={i} bounty={fp} /> return (
))} <div className="logs-section">
<div className="section-header">
<h2>FINGERPRINTS ({filteredFps.length}{serviceFilter ? ` / ${attacker.fingerprints.length}` : ''})</h2>
</div>
{filteredFps.length > 0 ? (
<div style={{ padding: '16px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
{filteredFps.map((fp, i) => (
<FingerprintCard key={i} bounty={fp} />
))}
</div>
) : (
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}>
{serviceFilter ? `NO ${serviceFilter.toUpperCase()} FINGERPRINTS CAPTURED` : 'NO FINGERPRINTS CAPTURED'}
</div>
)}
</div> </div>
) : ( );
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}> })()}
NO FINGERPRINTS CAPTURED
</div>
)}
</div>
{/* UUID footer */} {/* UUID footer */}
<div style={{ textAlign: 'right', fontSize: '0.65rem', opacity: 0.3, marginTop: '8px' }}> <div style={{ textAlign: 'right', fontSize: '0.65rem', opacity: 0.3, marginTop: '8px' }}>