Cybersecurity-Projects/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-strings.tsx

234 lines
5.9 KiB
TypeScript

// ===================
// © AngelaMos | 2026
// tab-strings.tsx
// ===================
import { useMemo, useState } from 'react'
import type {
AnalysisResponse,
ExtractedString,
StringCategory,
StringEncoding,
} from '@/api'
import { formatHex } from '@/core/lib'
import styles from './analysis.module.scss'
const ENCODING_OPTIONS: readonly ('All' | StringEncoding)[] = [
'All',
'Ascii',
'Utf8',
'Utf16Le',
] as const
const CATEGORY_OPTIONS: readonly ('All' | StringCategory)[] = [
'All',
'Url',
'IpAddress',
'FilePath',
'RegistryKey',
'ShellCommand',
'CryptoWallet',
'Email',
'SuspiciousApi',
'PackerSignature',
'DebugArtifact',
'AntiAnalysis',
'PersistencePath',
'EncodedData',
'Generic',
] as const
const PAGE_SIZE = 50
function StringRow({
str,
expanded,
onToggle,
}: {
str: ExtractedString
expanded: boolean
onToggle: () => void
}): React.ReactElement {
const MAX_DISPLAY = 80
const needsTruncate = str.value.length > MAX_DISPLAY
const display =
expanded || !needsTruncate
? str.value
: `${str.value.slice(0, MAX_DISPLAY)}\u2026`
return (
<tr
className={`${styles.tableRow} ${str.is_suspicious ? styles.suspicious : ''}`}
>
<td className={styles.cellMono}>{formatHex(str.offset)}</td>
<td>
<button
type="button"
className={styles.stringValue}
onClick={needsTruncate ? onToggle : undefined}
>
{display}
</button>
</td>
<td className={styles.cellMono}>{str.encoding}</td>
<td>
<span className={styles.categoryBadge}>{str.category}</span>
</td>
<td className={styles.cellMono}>{str.section ?? '\u2014'}</td>
</tr>
)
}
export function TabStrings({
data,
}: {
data: AnalysisResponse
}): React.ReactElement {
const str = data.passes.strings
const [search, setSearch] = useState('')
const [encoding, setEncoding] = useState<'All' | StringEncoding>('All')
const [category, setCategory] = useState<'All' | StringCategory>('All')
const [suspiciousOnly, setSuspiciousOnly] = useState(false)
const [page, setPage] = useState(0)
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set())
const filtered = useMemo(() => {
if (!str) return []
return str.strings.filter((s) => {
if (search && !s.value.toLowerCase().includes(search.toLowerCase()))
return false
if (encoding !== 'All' && s.encoding !== encoding) return false
if (category !== 'All' && s.category !== category) return false
if (suspiciousOnly && !s.is_suspicious) return false
return true
})
}, [str, search, encoding, category, suspiciousOnly])
if (!str) {
return (
<div className={styles.tabPanel}>
<span className={styles.noData}>No string data available</span>
</div>
)
}
const totalPages = Math.ceil(filtered.length / PAGE_SIZE)
const pageStrings = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE)
const toggleRow = (offset: number) => {
setExpandedRows((prev) => {
const next = new Set(prev)
if (next.has(offset)) next.delete(offset)
else next.add(offset)
return next
})
}
return (
<div className={styles.tabPanel}>
<div className={styles.filterBar}>
<input
type="text"
className={styles.searchInput}
placeholder="SEARCH STRINGS..."
value={search}
onChange={(e) => {
setSearch(e.target.value)
setPage(0)
}}
/>
<select
className={styles.filterSelect}
value={encoding}
onChange={(e) => {
setEncoding(e.target.value as 'All' | StringEncoding)
setPage(0)
}}
>
{ENCODING_OPTIONS.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
<select
className={styles.filterSelect}
value={category}
onChange={(e) => {
setCategory(e.target.value as 'All' | StringCategory)
setPage(0)
}}
>
{CATEGORY_OPTIONS.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
<button
type="button"
className={`${styles.filterToggle} ${suspiciousOnly ? styles.filterActive : ''}`}
onClick={() => {
setSuspiciousOnly((p) => !p)
setPage(0)
}}
>
SUSPICIOUS
</button>
</div>
<span className={styles.filterCount}>
{filtered.length} / {str.statistics.total} strings
</span>
<div className={styles.tableWrap}>
<table className={styles.dataTable}>
<thead>
<tr>
<th>OFFSET</th>
<th>VALUE</th>
<th>ENCODING</th>
<th>CATEGORY</th>
<th>SECTION</th>
</tr>
</thead>
<tbody>
{pageStrings.map((s) => (
<StringRow
key={s.offset}
str={s}
expanded={expandedRows.has(s.offset)}
onToggle={() => toggleRow(s.offset)}
/>
))}
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className={styles.pagination}>
<button
type="button"
className={styles.pageBtn}
disabled={page === 0}
onClick={() => setPage((p) => p - 1)}
>
PREV
</button>
<span className={styles.pageInfo}>
{page + 1} / {totalPages}
</span>
<button
type="button"
className={styles.pageBtn}
disabled={page >= totalPages - 1}
onClick={() => setPage((p) => p + 1)}
>
NEXT
</button>
</div>
)}
</div>
)
}