Skip to content

Commit 712bec7

Browse files
authored
Merge pull request #30 from osservatorionessuno/scan-detail
Add screen to show per-scan detail
2 parents d30a0ff + 0b31b00 commit 712bec7

6 files changed

Lines changed: 293 additions & 17 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
android:name=".AcquisitionActivity"
4444
android:exported="false"
4545
android:theme="@style/Theme.Theme" />
46+
<activity
47+
android:name=".ScanDetailActivity"
48+
android:exported="false"
49+
android:theme="@style/Theme.Theme" />
4650
<service
4751
android:name=".utils.AdbPairingService"
4852
android:exported="false"
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.osservatorionessuno.bugbane
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.activity.enableEdgeToEdge
7+
import androidx.compose.material.icons.Icons
8+
import androidx.compose.material.icons.automirrored.filled.ArrowBack
9+
import androidx.compose.material3.*
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.foundation.layout.*
13+
import androidx.compose.ui.platform.LocalContext
14+
import androidx.compose.ui.res.stringResource
15+
import java.io.File
16+
import org.osservatorionessuno.bugbane.ui.theme.Theme
17+
import org.osservatorionessuno.bugbane.screens.ScanDetailScreen
18+
19+
class ScanDetailActivity : ComponentActivity() {
20+
override fun onCreate(savedInstanceState: Bundle?) {
21+
super.onCreate(savedInstanceState)
22+
val acquisitionPath = intent.getStringExtra(EXTRA_ACQUISITION_PATH)
23+
val scanPath = intent.getStringExtra(EXTRA_SCAN_PATH)
24+
if (acquisitionPath == null || scanPath == null) {
25+
finish()
26+
return
27+
}
28+
val acquisitionDir = File(acquisitionPath)
29+
val scanFile = File(scanPath)
30+
enableEdgeToEdge()
31+
setContent {
32+
Theme {
33+
ScanDetailContent(acquisitionDir, scanFile)
34+
}
35+
}
36+
}
37+
38+
companion object {
39+
const val EXTRA_ACQUISITION_PATH = "acquisition_dir"
40+
const val EXTRA_SCAN_PATH = "scan_file"
41+
}
42+
}
43+
44+
@OptIn(ExperimentalMaterial3Api::class)
45+
@Composable
46+
fun ScanDetailContent(acquisitionDir: File, scanFile: File) {
47+
val context = LocalContext.current
48+
Scaffold(
49+
topBar = {
50+
TopAppBar(
51+
title = { Text(stringResource(R.string.analysis_details_title)) },
52+
navigationIcon = {
53+
IconButton(onClick = { (context as? ComponentActivity)?.finish() }) {
54+
Icon(
55+
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
56+
contentDescription = "Back"
57+
)
58+
}
59+
},
60+
colors = TopAppBarDefaults.topAppBarColors(
61+
containerColor = MaterialTheme.colorScheme.primary,
62+
titleContentColor = MaterialTheme.colorScheme.onPrimary,
63+
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
64+
)
65+
)
66+
}
67+
) { padding ->
68+
Box(
69+
modifier = Modifier
70+
.fillMaxSize()
71+
.padding(padding)
72+
) {
73+
ScanDetailScreen(acquisitionDir, scanFile)
74+
}
75+
}
76+
}

app/src/main/java/org/osservatorionessuno/bugbane/screens/AcquisitionDetailScreen.kt

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.content.res.Configuration
88
import androidx.activity.compose.rememberLauncherForActivityResult
99
import androidx.activity.result.contract.ActivityResultContracts
1010
import androidx.compose.foundation.border
11+
import androidx.compose.foundation.clickable
1112
import androidx.compose.foundation.layout.*
1213
import androidx.compose.foundation.lazy.LazyColumn
1314
import androidx.compose.foundation.lazy.items
@@ -24,6 +25,7 @@ import kotlinx.coroutines.Dispatchers
2425
import kotlinx.coroutines.launch
2526
import kotlinx.coroutines.withContext
2627
import org.osservatorionessuno.bugbane.analysis.AcquisitionScanner
28+
import org.osservatorionessuno.bugbane.ScanDetailActivity
2729
import org.json.JSONObject
2830
import java.io.File
2931
import java.io.FileInputStream
@@ -217,10 +219,11 @@ fun AcquisitionDetailScreen(acquisitionDir: File) {
217219
verticalArrangement = Arrangement.spacedBy(12.dp)
218220
) {
219221
Text(
220-
text = stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_details_scans),
222+
text = stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_details_analyses),
221223
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium)
222224
)
223225
ScanList(
226+
acquisitionDir = acquisitionDir,
224227
scans = scans,
225228
dateFormat = dateFormat,
226229
modifier = Modifier
@@ -277,10 +280,11 @@ fun AcquisitionDetailScreen(acquisitionDir: File) {
277280
Text(stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_details_rescan))
278281
}
279282
Text(
280-
text = stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_details_scans),
283+
text = stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_details_analyses),
281284
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium)
282285
)
283286
ScanList(
287+
acquisitionDir = acquisitionDir,
284288
scans = scans,
285289
dateFormat = dateFormat,
286290
modifier = Modifier
@@ -363,10 +367,12 @@ fun AcquisitionDetailScreen(acquisitionDir: File) {
363367

364368
@Composable
365369
private fun ScanList(
370+
acquisitionDir: File,
366371
scans: List<ScanSummary>,
367372
dateFormat: DateFormat,
368373
modifier: Modifier = Modifier
369374
) {
375+
val context = LocalContext.current
370376
Box(
371377
modifier = modifier
372378
.border(1.dp, MaterialTheme.colorScheme.outline)
@@ -375,24 +381,34 @@ private fun ScanList(
375381
item {
376382
Row(modifier = Modifier.fillMaxWidth()) {
377383
Text(
378-
stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_scans_date),
384+
stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_analyses_date),
379385
modifier = Modifier.weight(1f),
380386
fontWeight = FontWeight.SemiBold
381387
)
382388
Text(
383-
stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_scans_indicators),
389+
stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_analyses_indicators),
384390
modifier = Modifier.weight(1f),
385391
fontWeight = FontWeight.SemiBold
386392
)
387393
Text(
388-
stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_scans_matches),
394+
stringResource(org.osservatorionessuno.bugbane.R.string.acquisition_analyses_matches),
389395
modifier = Modifier.weight(1f),
390396
fontWeight = FontWeight.SemiBold
391397
)
392398
}
393399
}
394400
items(scans) { scan ->
395-
Row(modifier = Modifier.fillMaxWidth()) {
401+
Row(
402+
modifier = Modifier
403+
.fillMaxWidth()
404+
.clickable {
405+
val intent = Intent(context, ScanDetailActivity::class.java).apply {
406+
putExtra(ScanDetailActivity.EXTRA_ACQUISITION_PATH, acquisitionDir.absolutePath)
407+
putExtra(ScanDetailActivity.EXTRA_SCAN_PATH, scan.file.absolutePath)
408+
}
409+
context.startActivity(intent)
410+
}
411+
) {
396412
Text(
397413
dateFormat.format(Date.from(scan.started)),
398414
modifier = Modifier.weight(1f)
@@ -412,6 +428,7 @@ private fun ScanList(
412428
}
413429

414430
private data class ScanSummary(
431+
val file: File,
415432
val started: Instant,
416433
val indicatorsHash: String,
417434
val matchCount: Int,
@@ -436,7 +453,7 @@ private fun loadScans(acquisitionDir: File): List<ScanSummary> {
436453
sha256(hashes.joinToString(""))
437454
}
438455
val results = obj.optJSONArray("results")?.length() ?: 0
439-
ScanSummary(started, hash, results)
456+
ScanSummary(file, started, hash, results)
440457
} catch (_: Exception) {
441458
null
442459
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package org.osservatorionessuno.bugbane.screens
2+
3+
import androidx.compose.foundation.border
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.layout.*
6+
import androidx.compose.foundation.lazy.LazyColumn
7+
import androidx.compose.foundation.lazy.items
8+
import androidx.compose.material3.*
9+
import androidx.compose.runtime.*
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.res.stringResource
12+
import androidx.compose.ui.text.font.FontWeight
13+
import androidx.compose.ui.text.style.TextOverflow
14+
import androidx.compose.ui.unit.dp
15+
import org.json.JSONObject
16+
import java.io.File
17+
import java.text.DateFormat
18+
import java.time.Instant
19+
import java.util.Date
20+
import org.osservatorionessuno.bugbane.R
21+
22+
@Composable
23+
fun ScanDetailScreen(acquisitionDir: File, scanFile: File) {
24+
val dateFormat = remember { DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT) }
25+
var acquisitionMeta by remember { mutableStateOf<JSONObject?>(null) }
26+
var scanMeta by remember { mutableStateOf<JSONObject?>(null) }
27+
var results by remember { mutableStateOf(listOf<ScanResult>()) }
28+
var selected by remember { mutableStateOf<ScanResult?>(null) }
29+
30+
LaunchedEffect(acquisitionDir, scanFile) {
31+
val metaFile = File(acquisitionDir, "acquisition.json")
32+
if (metaFile.exists()) {
33+
try { acquisitionMeta = JSONObject(metaFile.readText()) } catch (_: Throwable) {}
34+
}
35+
try {
36+
val obj = JSONObject(scanFile.readText())
37+
scanMeta = obj
38+
val arr = obj.optJSONArray("results")
39+
val tmp = mutableListOf<ScanResult>()
40+
if (arr != null) {
41+
for (i in 0 until arr.length()) {
42+
val o = arr.getJSONObject(i)
43+
tmp += ScanResult(
44+
o.optString("file"),
45+
o.optString("type"),
46+
o.optString("ioc"),
47+
o.optString("context")
48+
)
49+
}
50+
}
51+
results = tmp
52+
} catch (_: Exception) {
53+
}
54+
}
55+
56+
Column(
57+
modifier = Modifier
58+
.fillMaxSize()
59+
.padding(16.dp),
60+
verticalArrangement = Arrangement.spacedBy(12.dp)
61+
) {
62+
Text(
63+
stringResource(R.string.analysis_details_acquisition_name, acquisitionDir.name),
64+
style = MaterialTheme.typography.bodyLarge
65+
)
66+
acquisitionMeta?.let {
67+
val uuid = it.optString("uuid")
68+
val completed = it.optString("completed").let { s ->
69+
try { dateFormat.format(Date.from(Instant.parse(s))) } catch (_: Exception) { s }
70+
}
71+
Text(
72+
stringResource(R.string.analysis_details_acquisition_uuid, uuid),
73+
style = MaterialTheme.typography.bodyLarge
74+
)
75+
Text(
76+
stringResource(R.string.analysis_details_acquisition_completed, completed),
77+
style = MaterialTheme.typography.bodyLarge
78+
)
79+
}
80+
scanMeta?.let {
81+
val completed = it.optString("completed").let { s ->
82+
try { dateFormat.format(Date.from(Instant.parse(s))) } catch (_: Exception) { s }
83+
}
84+
Text(
85+
stringResource(R.string.analysis_details_analysis_completed, completed),
86+
style = MaterialTheme.typography.bodyLarge
87+
)
88+
}
89+
ResultList(results = results, onSelect = { selected = it }, modifier = Modifier.weight(1f))
90+
}
91+
92+
selected?.let { res ->
93+
AlertDialog(
94+
onDismissRequest = { selected = null },
95+
confirmButton = {
96+
TextButton(onClick = { selected = null }) {
97+
Text(stringResource(R.string.acquisition_passphrase_close))
98+
}
99+
},
100+
title = { Text(stringResource(R.string.analysis_details_context_dialog_title)) },
101+
text = { Text(res.context) }
102+
)
103+
}
104+
}
105+
106+
data class ScanResult(
107+
val file: String,
108+
val type: String,
109+
val ioc: String,
110+
val context: String,
111+
)
112+
113+
@Composable
114+
private fun ResultList(
115+
results: List<ScanResult>,
116+
onSelect: (ScanResult) -> Unit,
117+
modifier: Modifier = Modifier
118+
) {
119+
Box(
120+
modifier = modifier
121+
.border(1.dp, MaterialTheme.colorScheme.outline)
122+
.fillMaxSize()
123+
) {
124+
LazyColumn(modifier = Modifier.fillMaxSize().padding(8.dp)) {
125+
item {
126+
Row(modifier = Modifier.fillMaxWidth()) {
127+
Text(
128+
stringResource(R.string.analysis_details_artifact),
129+
modifier = Modifier.weight(1f),
130+
fontWeight = FontWeight.SemiBold
131+
)
132+
Text(
133+
stringResource(R.string.analysis_details_type),
134+
modifier = Modifier.weight(1f),
135+
fontWeight = FontWeight.SemiBold
136+
)
137+
Text(
138+
stringResource(R.string.analysis_details_ioc),
139+
modifier = Modifier.weight(1f),
140+
fontWeight = FontWeight.SemiBold
141+
)
142+
}
143+
}
144+
items(results) { r ->
145+
Row(
146+
modifier = Modifier
147+
.fillMaxWidth()
148+
.clickable { onSelect(r) }
149+
) {
150+
Text(r.file, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis)
151+
Text(r.type, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis)
152+
Text(r.ioc, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis)
153+
}
154+
}
155+
}
156+
}
157+
}

app/src/main/res/values-it/strings.xml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,26 @@
6464
<string name="acquisition_details_completed">Completato: %1$s</string>
6565
<string name="acquisition_details_files">File</string>
6666
<string name="acquisition_details_view_files">Visualizza i file</string>
67-
<string name="acquisition_details_scans">Analisi</string>
68-
<string name="acquisition_scans_date">Data</string>
69-
<string name="acquisition_scans_indicators">Indicatori</string>
70-
<string name="acquisition_scans_matches">Corrispondenza</string>
67+
<string name="acquisition_details_analyses">Analisi</string>
68+
<string name="acquisition_analyses_date">Data</string>
69+
<string name="acquisition_analyses_indicators">Indicatori</string>
70+
<string name="acquisition_analyses_matches">Corrispondenza</string>
7171
<string name="acquisition_details_export">Esporta</string>
7272
<string name="acquisition_details_share">Condividi</string>
7373
<string name="acquisition_details_rescan">Esegui analisi</string>
7474
<string name="acquisition_export_passphrase">L\'archivio esportato contiene informazioni potenzialmente private ed è stato crittografato. La password verrà mostrata soltanto una volta:\n\n%1$s</string>
7575
<string name="acquisition_share_message">Archivio di acquisizione. Passphrase: %1$s</string>
7676
<string name="acquisition_passphrase_copy">Copia nella clipboard</string>
7777
<string name="acquisition_passphrase_close">Chiudi</string>
78+
79+
<!-- Scan details strings -->
80+
<string name="analysis_details_title">Dettagli analisi</string>
81+
<string name="analysis_details_artifact">Artefatto</string>
82+
<string name="analysis_details_type">Tipo</string>
83+
<string name="analysis_details_ioc">IOC</string>
84+
<string name="analysis_details_context_dialog_title">Contesto</string>
85+
<string name="analysis_details_acquisition_name">Nome acquisizione: %1$s</string>
86+
<string name="analysis_details_acquisition_uuid">UUID acquisizione: %1$s</string>
87+
<string name="analysis_details_acquisition_completed">Acquisizione completata: %1$s</string>
88+
<string name="analysis_details_analysis_completed">Analisi completata: %1$s</string>
7889
</resources>

0 commit comments

Comments
 (0)