Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
android:name=".AcquisitionActivity"
android:exported="false"
android:theme="@style/Theme.Theme" />
<activity
android:name=".ScanDetailActivity"
android:exported="false"
android:theme="@style/Theme.Theme" />
<service
android:name=".utils.AdbPairingService"
android:exported="false"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.osservatorionessuno.bugbane

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import java.io.File
import org.osservatorionessuno.bugbane.ui.theme.Theme
import org.osservatorionessuno.bugbane.screens.ScanDetailScreen

class ScanDetailActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val acquisitionPath = intent.getStringExtra(EXTRA_ACQUISITION_PATH)
val scanPath = intent.getStringExtra(EXTRA_SCAN_PATH)
if (acquisitionPath == null || scanPath == null) {
finish()
return
}
val acquisitionDir = File(acquisitionPath)
val scanFile = File(scanPath)
enableEdgeToEdge()
setContent {
Theme {
ScanDetailContent(acquisitionDir, scanFile)
}
}
}

companion object {
const val EXTRA_ACQUISITION_PATH = "acquisition_dir"
const val EXTRA_SCAN_PATH = "scan_file"
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScanDetailContent(acquisitionDir: File, scanFile: File) {
val context = LocalContext.current
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.analysis_details_title)) },
navigationIcon = {
IconButton(onClick = { (context as? ComponentActivity)?.finish() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
)
)
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
ScanDetailScreen(acquisitionDir, scanFile)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -217,10 +219,11 @@ 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(
acquisitionDir = acquisitionDir,
scans = scans,
dateFormat = dateFormat,
modifier = Modifier
Expand Down Expand Up @@ -277,10 +280,11 @@ 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(
acquisitionDir = acquisitionDir,
scans = scans,
dateFormat = dateFormat,
modifier = Modifier
Expand Down Expand Up @@ -363,10 +367,12 @@ fun AcquisitionDetailScreen(acquisitionDir: File) {

@Composable
private fun ScanList(
acquisitionDir: File,
scans: List<ScanSummary>,
dateFormat: DateFormat,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
Box(
modifier = modifier
.border(1.dp, MaterialTheme.colorScheme.outline)
Expand All @@ -375,24 +381,34 @@ 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
)
}
}
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)
Expand All @@ -412,6 +428,7 @@ private fun ScanList(
}

private data class ScanSummary(
val file: File,
val started: Instant,
val indicatorsHash: String,
val matchCount: Int,
Expand All @@ -436,7 +453,7 @@ private fun loadScans(acquisitionDir: File): List<ScanSummary> {
sha256(hashes.joinToString(""))
}
val results = obj.optJSONArray("results")?.length() ?: 0
ScanSummary(started, hash, results)
ScanSummary(file, started, hash, results)
} catch (_: Exception) {
null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<JSONObject?>(null) }
var scanMeta by remember { mutableStateOf<JSONObject?>(null) }
var results by remember { mutableStateOf(listOf<ScanResult>()) }
var selected by remember { mutableStateOf<ScanResult?>(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<ScanResult>()
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.analysis_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.analysis_details_acquisition_uuid, uuid),
style = MaterialTheme.typography.bodyLarge
)
Text(
stringResource(R.string.analysis_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.analysis_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.analysis_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<ScanResult>,
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.analysis_details_artifact),
modifier = Modifier.weight(1f),
fontWeight = FontWeight.SemiBold
)
Text(
stringResource(R.string.analysis_details_type),
modifier = Modifier.weight(1f),
fontWeight = FontWeight.SemiBold
)
Text(
stringResource(R.string.analysis_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)
}
}
}
}
}
19 changes: 15 additions & 4 deletions app/src/main/res/values-it/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,26 @@
<string name="acquisition_details_completed">Completato: %1$s</string>
<string name="acquisition_details_files">File</string>
<string name="acquisition_details_view_files">Visualizza i file</string>
<string name="acquisition_details_scans">Analisi</string>
<string name="acquisition_scans_date">Data</string>
<string name="acquisition_scans_indicators">Indicatori</string>
<string name="acquisition_scans_matches">Corrispondenza</string>
<string name="acquisition_details_analyses">Analisi</string>
<string name="acquisition_analyses_date">Data</string>
<string name="acquisition_analyses_indicators">Indicatori</string>
<string name="acquisition_analyses_matches">Corrispondenza</string>
<string name="acquisition_details_export">Esporta</string>
<string name="acquisition_details_share">Condividi</string>
<string name="acquisition_details_rescan">Esegui analisi</string>
<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>
<string name="acquisition_share_message">Archivio di acquisizione. Passphrase: %1$s</string>
<string name="acquisition_passphrase_copy">Copia nella clipboard</string>
<string name="acquisition_passphrase_close">Chiudi</string>

<!-- Scan details strings -->
<string name="analysis_details_title">Dettagli analisi</string>
<string name="analysis_details_artifact">Artefatto</string>
<string name="analysis_details_type">Tipo</string>
<string name="analysis_details_ioc">IOC</string>
<string name="analysis_details_context_dialog_title">Contesto</string>
<string name="analysis_details_acquisition_name">Nome acquisizione: %1$s</string>
<string name="analysis_details_acquisition_uuid">UUID acquisizione: %1$s</string>
<string name="analysis_details_acquisition_completed">Acquisizione completata: %1$s</string>
<string name="analysis_details_analysis_completed">Analisi completata: %1$s</string>
</resources>
Loading