From 87b565a1931ac922dfc18a2a8a03871b6f52dc4b Mon Sep 17 00:00:00 2001 From: giulio <66009328+lsd-cat@users.noreply.github.com> Date: Thu, 4 Sep 2025 03:04:43 +0200 Subject: [PATCH 1/3] Add screen to show per-scan detail --- app/src/main/AndroidManifest.xml | 4 + .../bugbane/ScanDetailActivity.kt | 76 +++++++++ .../screens/AcquisitionDetailScreen.kt | 21 ++- .../bugbane/screens/ScanDetailScreen.kt | 157 ++++++++++++++++++ app/src/main/res/values-it/strings.xml | 11 ++ app/src/main/res/values/strings.xml | 11 ++ 6 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/osservatorionessuno/bugbane/ScanDetailActivity.kt create mode 100644 app/src/main/java/org/osservatorionessuno/bugbane/screens/ScanDetailScreen.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ffd7410..eb530c9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,6 +43,10 @@ android:name=".AcquisitionActivity" android:exported="false" android:theme="@style/Theme.Theme" /> + + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + ScanDetailScreen(acquisitionDir, scanFile) + } + } +} diff --git a/app/src/main/java/org/osservatorionessuno/bugbane/screens/AcquisitionDetailScreen.kt b/app/src/main/java/org/osservatorionessuno/bugbane/screens/AcquisitionDetailScreen.kt index 1c773e4..cdada12 100644 --- a/app/src/main/java/org/osservatorionessuno/bugbane/screens/AcquisitionDetailScreen.kt +++ b/app/src/main/java/org/osservatorionessuno/bugbane/screens/AcquisitionDetailScreen.kt @@ -8,6 +8,7 @@ import android.content.res.Configuration import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -24,6 +25,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.osservatorionessuno.bugbane.analysis.AcquisitionScanner +import org.osservatorionessuno.bugbane.ScanDetailActivity import org.json.JSONObject import java.io.File import java.io.FileInputStream @@ -221,6 +223,7 @@ fun AcquisitionDetailScreen(acquisitionDir: File) { style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium) ) ScanList( + acquisitionDir = acquisitionDir, scans = scans, dateFormat = dateFormat, modifier = Modifier @@ -281,6 +284,7 @@ fun AcquisitionDetailScreen(acquisitionDir: File) { style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium) ) ScanList( + acquisitionDir = acquisitionDir, scans = scans, dateFormat = dateFormat, modifier = Modifier @@ -363,10 +367,12 @@ fun AcquisitionDetailScreen(acquisitionDir: File) { @Composable private fun ScanList( + acquisitionDir: File, scans: List, dateFormat: DateFormat, modifier: Modifier = Modifier ) { + val context = LocalContext.current Box( modifier = modifier .border(1.dp, MaterialTheme.colorScheme.outline) @@ -392,7 +398,17 @@ private fun ScanList( } } items(scans) { scan -> - Row(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + val intent = Intent(context, ScanDetailActivity::class.java).apply { + putExtra(ScanDetailActivity.EXTRA_ACQUISITION_PATH, acquisitionDir.absolutePath) + putExtra(ScanDetailActivity.EXTRA_SCAN_PATH, scan.file.absolutePath) + } + context.startActivity(intent) + } + ) { Text( dateFormat.format(Date.from(scan.started)), modifier = Modifier.weight(1f) @@ -412,6 +428,7 @@ private fun ScanList( } private data class ScanSummary( + val file: File, val started: Instant, val indicatorsHash: String, val matchCount: Int, @@ -436,7 +453,7 @@ private fun loadScans(acquisitionDir: File): List { sha256(hashes.joinToString("")) } val results = obj.optJSONArray("results")?.length() ?: 0 - ScanSummary(started, hash, results) + ScanSummary(file, started, hash, results) } catch (_: Exception) { null } diff --git a/app/src/main/java/org/osservatorionessuno/bugbane/screens/ScanDetailScreen.kt b/app/src/main/java/org/osservatorionessuno/bugbane/screens/ScanDetailScreen.kt new file mode 100644 index 0000000..721045c --- /dev/null +++ b/app/src/main/java/org/osservatorionessuno/bugbane/screens/ScanDetailScreen.kt @@ -0,0 +1,157 @@ +package org.osservatorionessuno.bugbane.screens + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.json.JSONObject +import java.io.File +import java.text.DateFormat +import java.time.Instant +import java.util.Date +import org.osservatorionessuno.bugbane.R + +@Composable +fun ScanDetailScreen(acquisitionDir: File, scanFile: File) { + val dateFormat = remember { DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT) } + var acquisitionMeta by remember { mutableStateOf(null) } + var scanMeta by remember { mutableStateOf(null) } + var results by remember { mutableStateOf(listOf()) } + var selected by remember { mutableStateOf(null) } + + LaunchedEffect(acquisitionDir, scanFile) { + val metaFile = File(acquisitionDir, "acquisition.json") + if (metaFile.exists()) { + try { acquisitionMeta = JSONObject(metaFile.readText()) } catch (_: Throwable) {} + } + try { + val obj = JSONObject(scanFile.readText()) + scanMeta = obj + val arr = obj.optJSONArray("results") + val tmp = mutableListOf() + if (arr != null) { + for (i in 0 until arr.length()) { + val o = arr.getJSONObject(i) + tmp += ScanResult( + o.optString("file"), + o.optString("type"), + o.optString("ioc"), + o.optString("context") + ) + } + } + results = tmp + } catch (_: Exception) { + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + stringResource(R.string.scan_details_acquisition_name, acquisitionDir.name), + style = MaterialTheme.typography.bodyLarge + ) + acquisitionMeta?.let { + val uuid = it.optString("uuid") + val completed = it.optString("completed").let { s -> + try { dateFormat.format(Date.from(Instant.parse(s))) } catch (_: Exception) { s } + } + Text( + stringResource(R.string.scan_details_acquisition_uuid, uuid), + style = MaterialTheme.typography.bodyLarge + ) + Text( + stringResource(R.string.scan_details_acquisition_completed, completed), + style = MaterialTheme.typography.bodyLarge + ) + } + scanMeta?.let { + val completed = it.optString("completed").let { s -> + try { dateFormat.format(Date.from(Instant.parse(s))) } catch (_: Exception) { s } + } + Text( + stringResource(R.string.scan_details_analysis_completed, completed), + style = MaterialTheme.typography.bodyLarge + ) + } + ResultList(results = results, onSelect = { selected = it }, modifier = Modifier.weight(1f)) + } + + selected?.let { res -> + AlertDialog( + onDismissRequest = { selected = null }, + confirmButton = { + TextButton(onClick = { selected = null }) { + Text(stringResource(R.string.acquisition_passphrase_close)) + } + }, + title = { Text(stringResource(R.string.scan_details_context_dialog_title)) }, + text = { Text(res.context) } + ) + } +} + +data class ScanResult( + val file: String, + val type: String, + val ioc: String, + val context: String, +) + +@Composable +private fun ResultList( + results: List, + onSelect: (ScanResult) -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .border(1.dp, MaterialTheme.colorScheme.outline) + .fillMaxSize() + ) { + LazyColumn(modifier = Modifier.fillMaxSize().padding(8.dp)) { + item { + Row(modifier = Modifier.fillMaxWidth()) { + Text( + stringResource(R.string.scan_details_artifact), + modifier = Modifier.weight(1f), + fontWeight = FontWeight.SemiBold + ) + Text( + stringResource(R.string.scan_details_type), + modifier = Modifier.weight(1f), + fontWeight = FontWeight.SemiBold + ) + Text( + stringResource(R.string.scan_details_ioc), + modifier = Modifier.weight(1f), + fontWeight = FontWeight.SemiBold + ) + } + } + items(results) { r -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onSelect(r) } + ) { + Text(r.file, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(r.type, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(r.ioc, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis) + } + } + } + } +} diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 14f607d..68a9927 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -75,4 +75,15 @@ Archivio di acquisizione. Passphrase: %1$s Copia nella clipboard Chiudi + + + Dettagli analisi + Artefatto + Tipo + IOC + Contesto + Nome acquisizione: %1$s + UUID acquisizione: %1$s + Acquisizione completata: %1$s + Analisi completata: %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index db49733..813e419 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -106,4 +106,15 @@ Acquisition archive. Passphrase: %1$s Copy to clipboard Close + + + Scan Details + Artifact + Type + IOC + Context + Acquisition name: %1$s + Acquisition UUID: %1$s + Acquisition completed: %1$s + Analysis completed: %1$s From cb14efb520845734803790c069fdfc498a943cb9 Mon Sep 17 00:00:00 2001 From: giulio <66009328+lsd-cat@users.noreply.github.com> Date: Thu, 4 Sep 2025 04:19:14 +0200 Subject: [PATCH 2/3] Make nomenclature consistent (scan->analysis) --- ...lActivity.kt => AnalysisDetailActivity.kt} | 2 +- .../screens/AcquisitionDetailScreen.kt | 10 +++---- ...etailScreen.kt => AnalysisDetailScreen.kt} | 16 +++++----- app/src/main/res/values-it/strings.xml | 26 ++++++++-------- app/src/main/res/values/strings.xml | 30 +++++++++---------- 5 files changed, 42 insertions(+), 42 deletions(-) rename app/src/main/java/org/osservatorionessuno/bugbane/{ScanDetailActivity.kt => AnalysisDetailActivity.kt} (96%) rename app/src/main/java/org/osservatorionessuno/bugbane/screens/{ScanDetailScreen.kt => AnalysisDetailScreen.kt} (88%) diff --git a/app/src/main/java/org/osservatorionessuno/bugbane/ScanDetailActivity.kt b/app/src/main/java/org/osservatorionessuno/bugbane/AnalysisDetailActivity.kt similarity index 96% rename from app/src/main/java/org/osservatorionessuno/bugbane/ScanDetailActivity.kt rename to app/src/main/java/org/osservatorionessuno/bugbane/AnalysisDetailActivity.kt index 8995840..0e0c3c5 100644 --- a/app/src/main/java/org/osservatorionessuno/bugbane/ScanDetailActivity.kt +++ b/app/src/main/java/org/osservatorionessuno/bugbane/AnalysisDetailActivity.kt @@ -48,7 +48,7 @@ fun ScanDetailContent(acquisitionDir: File, scanFile: File) { Scaffold( topBar = { TopAppBar( - title = { Text(stringResource(R.string.scan_details_title)) }, + title = { Text(stringResource(R.string.analysis_details_title)) }, navigationIcon = { IconButton(onClick = { (context as? ComponentActivity)?.finish() }) { Icon( diff --git a/app/src/main/java/org/osservatorionessuno/bugbane/screens/AcquisitionDetailScreen.kt b/app/src/main/java/org/osservatorionessuno/bugbane/screens/AcquisitionDetailScreen.kt index cdada12..11393ed 100644 --- a/app/src/main/java/org/osservatorionessuno/bugbane/screens/AcquisitionDetailScreen.kt +++ b/app/src/main/java/org/osservatorionessuno/bugbane/screens/AcquisitionDetailScreen.kt @@ -219,7 +219,7 @@ fun AcquisitionDetailScreen(acquisitionDir: File) { verticalArrangement = Arrangement.spacedBy(12.dp) ) { Text( - text = stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_details_scans), + text = stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_details_analyses), style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium) ) ScanList( @@ -280,7 +280,7 @@ fun AcquisitionDetailScreen(acquisitionDir: File) { Text(stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_details_rescan)) } Text( - text = stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_details_scans), + text = stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_details_analyses), style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium) ) ScanList( @@ -381,17 +381,17 @@ private fun ScanList( item { Row(modifier = Modifier.fillMaxWidth()) { Text( - stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_scans_date), + stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_analyses_date), modifier = Modifier.weight(1f), fontWeight = FontWeight.SemiBold ) Text( - stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_scans_indicators), + stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_analyses_indicators), modifier = Modifier.weight(1f), fontWeight = FontWeight.SemiBold ) Text( - stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_scans_matches), + stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_analyses_matches), modifier = Modifier.weight(1f), fontWeight = FontWeight.SemiBold ) diff --git a/app/src/main/java/org/osservatorionessuno/bugbane/screens/ScanDetailScreen.kt b/app/src/main/java/org/osservatorionessuno/bugbane/screens/AnalysisDetailScreen.kt similarity index 88% rename from app/src/main/java/org/osservatorionessuno/bugbane/screens/ScanDetailScreen.kt rename to app/src/main/java/org/osservatorionessuno/bugbane/screens/AnalysisDetailScreen.kt index 721045c..1cea0ec 100644 --- a/app/src/main/java/org/osservatorionessuno/bugbane/screens/ScanDetailScreen.kt +++ b/app/src/main/java/org/osservatorionessuno/bugbane/screens/AnalysisDetailScreen.kt @@ -60,7 +60,7 @@ fun ScanDetailScreen(acquisitionDir: File, scanFile: File) { verticalArrangement = Arrangement.spacedBy(12.dp) ) { Text( - stringResource(R.string.scan_details_acquisition_name, acquisitionDir.name), + stringResource(R.string.analysis_details_acquisition_name, acquisitionDir.name), style = MaterialTheme.typography.bodyLarge ) acquisitionMeta?.let { @@ -69,11 +69,11 @@ fun ScanDetailScreen(acquisitionDir: File, scanFile: File) { try { dateFormat.format(Date.from(Instant.parse(s))) } catch (_: Exception) { s } } Text( - stringResource(R.string.scan_details_acquisition_uuid, uuid), + stringResource(R.string.analysis_details_acquisition_uuid, uuid), style = MaterialTheme.typography.bodyLarge ) Text( - stringResource(R.string.scan_details_acquisition_completed, completed), + stringResource(R.string.analysis_details_acquisition_completed, completed), style = MaterialTheme.typography.bodyLarge ) } @@ -82,7 +82,7 @@ fun ScanDetailScreen(acquisitionDir: File, scanFile: File) { try { dateFormat.format(Date.from(Instant.parse(s))) } catch (_: Exception) { s } } Text( - stringResource(R.string.scan_details_analysis_completed, completed), + stringResource(R.string.analysis_details_analysis_completed, completed), style = MaterialTheme.typography.bodyLarge ) } @@ -97,7 +97,7 @@ fun ScanDetailScreen(acquisitionDir: File, scanFile: File) { Text(stringResource(R.string.acquisition_passphrase_close)) } }, - title = { Text(stringResource(R.string.scan_details_context_dialog_title)) }, + title = { Text(stringResource(R.string.analysis_details_context_dialog_title)) }, text = { Text(res.context) } ) } @@ -125,17 +125,17 @@ private fun ResultList( item { Row(modifier = Modifier.fillMaxWidth()) { Text( - stringResource(R.string.scan_details_artifact), + stringResource(R.string.analysis_details_artifact), modifier = Modifier.weight(1f), fontWeight = FontWeight.SemiBold ) Text( - stringResource(R.string.scan_details_type), + stringResource(R.string.analysis_details_type), modifier = Modifier.weight(1f), fontWeight = FontWeight.SemiBold ) Text( - stringResource(R.string.scan_details_ioc), + stringResource(R.string.analysis_details_ioc), modifier = Modifier.weight(1f), fontWeight = FontWeight.SemiBold ) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 68a9927..81449c2 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -64,10 +64,10 @@ Completato: %1$s File Visualizza i file - Analisi - Data - Indicatori - Corrispondenza + Analisi + Data + Indicatori + Corrispondenza Esporta Condividi Esegui analisi @@ -77,13 +77,13 @@ Chiudi - Dettagli analisi - Artefatto - Tipo - IOC - Contesto - Nome acquisizione: %1$s - UUID acquisizione: %1$s - Acquisizione completata: %1$s - Analisi completata: %1$s + Dettagli analisi + Artefatto + Tipo + IOC + Contesto + Nome acquisizione: %1$s + UUID acquisizione: %1$s + Acquisizione completata: %1$s + Analisi completata: %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 813e419..1b08cab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,7 +44,7 @@ Start Acquisition Acquiring… Enable Permissions - After each acquisition, it’s recommended to disable Developer Options until the next session. + After each acquisition, it's recommended to disable Developer Options until the next session. Disable Close Stop Acquisition @@ -53,7 +53,7 @@ Configure your preferences and app settings here. General Settings Disable Developer Options - It’s recommended to disable Developer Options after each session and re-enable them only when needed. + It's recommended to disable Developer Options after each session and re-enable them only when needed. Indicators Last fetch: %1$s Last update: %1$s @@ -93,10 +93,10 @@ Completed: %1$s Files View files - Analyses - Date - Indicators - Matches + Analyses + Date + Indicators + Matches Export Share Run Analysis @@ -108,13 +108,13 @@ Close - Scan Details - Artifact - Type - IOC - Context - Acquisition name: %1$s - Acquisition UUID: %1$s - Acquisition completed: %1$s - Analysis completed: %1$s + Analysis Details + Artifact + Type + IOC + Context + Acquisition name: %1$s + Acquisition UUID: %1$s + Acquisition completed: %1$s + Analysis completed: %1$s From 0b31b00b0c0e06093a34f3bb4bb7db10d5fc8c80 Mon Sep 17 00:00:00 2001 From: giulio <66009328+lsd-cat@users.noreply.github.com> Date: Thu, 4 Sep 2025 04:30:00 +0200 Subject: [PATCH 3/3] Fix quote escape in strings.xml --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1b08cab..d37981b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,7 +44,7 @@ Start Acquisition Acquiring… Enable Permissions - After each acquisition, it's recommended to disable Developer Options until the next session. + After each acquisition, it\'s recommended to disable Developer Options until the next session. Disable Close Stop Acquisition @@ -53,7 +53,7 @@ Configure your preferences and app settings here. General Settings Disable Developer Options - It's recommended to disable Developer Options after each session and re-enable them only when needed. + It\'s recommended to disable Developer Options after each session and re-enable them only when needed. Indicators Last fetch: %1$s Last update: %1$s