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) => {
const isActive = serviceFilter === svc;
return (
<span <span
key={svc} key={svc}
className="service-badge" className="service-badge"
style={{ fontSize: '0.85rem', padding: '4px 12px', cursor: 'pointer' }} style={{
onClick={() => navigate(`/attackers?service=${encodeURIComponent(svc)}`)} fontSize: '0.85rem', padding: '4px 12px', cursor: 'pointer',
title={`Filter attackers by ${svc.toUpperCase()}`} ...(isActive ? {
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()} {svc.toUpperCase()}
</span> </span>
)) : ( );
}) : (
<span className="dim">No services recorded</span> <span className="dim">No services recorded</span>
)} )}
</div> </div>
@@ -371,11 +382,16 @@ const AttackerDetail: React.FC = () => {
</div> </div>
{/* Commands */} {/* Commands */}
{(() => {
const filteredCmds = serviceFilter
? attacker.commands.filter((cmd) => cmd.service === serviceFilter)
: attacker.commands;
return (
<div className="logs-section"> <div className="logs-section">
<div className="section-header"> <div className="section-header">
<h2>COMMANDS ({attacker.commands.length})</h2> <h2>COMMANDS ({filteredCmds.length}{serviceFilter ? ` / ${attacker.commands.length}` : ''})</h2>
</div> </div>
{attacker.commands.length > 0 ? ( {filteredCmds.length > 0 ? (
<div className="logs-table-container"> <div className="logs-table-container">
<table className="logs-table"> <table className="logs-table">
<thead> <thead>
@@ -387,7 +403,7 @@ const AttackerDetail: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{attacker.commands.map((cmd, i) => ( {filteredCmds.map((cmd, i) => (
<tr key={i}> <tr key={i}>
<td className="dim" style={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}> <td className="dim" style={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}>
{cmd.timestamp ? new Date(cmd.timestamp).toLocaleString() : '-'} {cmd.timestamp ? new Date(cmd.timestamp).toLocaleString() : '-'}
@@ -402,28 +418,40 @@ const AttackerDetail: React.FC = () => {
</div> </div>
) : ( ) : (
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}> <div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}>
NO COMMANDS CAPTURED {serviceFilter ? `NO ${serviceFilter.toUpperCase()} COMMANDS CAPTURED` : 'NO COMMANDS CAPTURED'}
</div> </div>
)} )}
</div> </div>
);
})()}
{/* Fingerprints */} {/* Fingerprints */}
{(() => {
const filteredFps = serviceFilter
? attacker.fingerprints.filter((fp) => {
const p = getPayload(fp);
return p.service === serviceFilter;
})
: attacker.fingerprints;
return (
<div className="logs-section"> <div className="logs-section">
<div className="section-header"> <div className="section-header">
<h2>FINGERPRINTS ({attacker.fingerprints.length})</h2> <h2>FINGERPRINTS ({filteredFps.length}{serviceFilter ? ` / ${attacker.fingerprints.length}` : ''})</h2>
</div> </div>
{attacker.fingerprints.length > 0 ? ( {filteredFps.length > 0 ? (
<div style={{ padding: '16px', display: 'flex', flexDirection: 'column', gap: '12px' }}> <div style={{ padding: '16px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
{attacker.fingerprints.map((fp, i) => ( {filteredFps.map((fp, i) => (
<FingerprintCard key={i} bounty={fp} /> <FingerprintCard key={i} bounty={fp} />
))} ))}
</div> </div>
) : ( ) : (
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}> <div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}>
NO FINGERPRINTS CAPTURED {serviceFilter ? `NO ${serviceFilter.toUpperCase()} FINGERPRINTS CAPTURED` : 'NO FINGERPRINTS CAPTURED'}
</div> </div>
)} )}
</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' }}>