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
37 changes: 33 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,31 @@ on:
branches: [ main ]
workflow_dispatch: {}

env:
APK_KEYSTORE_PASSWORD: ${{ secrets.APK_KEYSTORE_PASSWORD }}
APK_KEY_ALIAS: ${{ secrets.APK_KEY_ALIAS }}
APK_KEY_PASSWORD: ${{ secrets.APK_KEY_PASSWORD }}

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Stable env
run: |
echo "TZ=UTC" >> $GITHUB_ENV
echo "LANG=C.UTF-8" >> $GITHUB_ENV
echo "LC_ALL=C.UTF-8" >> $GITHUB_ENV
echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV

- name: Restore testing keystore
env:
APK_KEYSTORE_B64: ${{ secrets.APK_KEYSTORE_B64 }}
run: |
echo "$APK_KEYSTORE_B64" | base64 -d > keystore.jks
echo "APK_KEYSTORE=$GITHUB_WORKSPACE/keystore.jks" >> $GITHUB_ENV

- name: Set up JDK
uses: actions/setup-java@v4
with:
Expand All @@ -35,12 +54,22 @@ jobs:
gradle --version

- name: Build with Gradle
run: gradle --no-daemon build
run: gradle --no-daemon assembleDebug assembleRelease

- name: Upload debug APK
if: success()
uses: actions/upload-artifact@v4
with:
name: app-debug
path: app/build/outputs/apk/debug/app-debug.apk
compression-level: 0
retention-days: 7

- name: Upload APKs
- name: Upload release APK
if: success()
uses: actions/upload-artifact@v4
with:
name: apk-${{ github.run_number }}
path: "**/build/outputs/apk/**/*.apk"
name: app-release
path: app/build/outputs/apk/release/app-release.apk
compression-level: 0
retention-days: 7
43 changes: 39 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
Expand All @@ -10,6 +12,32 @@ android {
namespace = "org.osservatorionessuno.bugbane"
compileSdk = 36

// For deterministic CI build and signatures
val ksPathStr: String? = System.getenv("APK_KEYSTORE")
val haveCiKeystore = ksPathStr != null

if (haveCiKeystore) {
signingConfigs {
create("ciRelease") {
val ksPass = System.getenv("APK_KEYSTORE_PASSWORD") ?: error("APK_KEYSTORE_PASSWORD not set")
val alias = System.getenv("APK_KEY_ALIAS") ?: error("APK_KEY_ALIAS not set")
val keyPass = System.getenv("APK_KEY_PASSWORD") ?: error("APK_KEY_PASSWORD not set")

// use the actual path string here
storeFile = file(ksPathStr!!)
storePassword = ksPass
keyAlias = alias
keyPassword = keyPass

// Deterministic signing (avoid v1/JAR)
enableV1Signing = false
enableV2Signing = true
enableV3Signing = true
enableV4Signing = false
}
}
}

defaultConfig {
applicationId = "org.osservatorionessuno.bugbane"
minSdk = 30
Expand All @@ -26,15 +54,22 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
signingConfig = if (haveCiKeystore)
signingConfigs.getByName("ciRelease")
else
signingConfigs.getByName("debug")
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions { jvmTarget = "11" }
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_11
}
}

buildFeatures {
compose = true
Expand All @@ -61,6 +96,8 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.foundation)
implementation(libs.compose.material.icons.extended)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.snakeyaml)

// libadb-android and its dependency
implementation(libs.libadb.android)
Expand All @@ -82,7 +119,6 @@ dependencies {
testRuntimeOnly(libs.junit.jupiter.engine)

testImplementation(libs.org.json)

androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
Expand All @@ -94,7 +130,6 @@ dependencies {

protobuf {
protoc {
// was: artifact = "com.google.protobuf:protoc:3.25.3"
artifact = libs.protoc.get().toString()
}
// Generate lite Java classes for Android
Expand Down
52 changes: 49 additions & 3 deletions app/src/main/java/org/osservatorionessuno/bugbane/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import kotlinx.coroutines.launch
import io.github.muntashirakon.adb.PRNGFixes
import androidx.lifecycle.lifecycleScope
import androidx.work.*
import kotlinx.coroutines.Dispatchers
import org.osservatorionessuno.libmvt.common.IndicatorsUpdates
import org.osservatorionessuno.bugbane.workers.IndicatorsUpdateWorker
import java.util.concurrent.TimeUnit
import org.osservatorionessuno.bugbane.components.AppTopBar
import org.osservatorionessuno.bugbane.components.MergedTopBar
import org.osservatorionessuno.bugbane.components.NavigationTabs
Expand All @@ -36,7 +42,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PRNGFixes.apply()

// Observers
viewModel.watchConnectAdb().observe(this) { isConnected ->
if (!isConnected) {
Expand All @@ -49,7 +55,7 @@ class MainActivity : ComponentActivity() {
setLacksPermissionsCallback?.invoke(true)
}
}

viewModel.watchCommandOutput().observe(this) { output ->
// TODO: blibla
Toast.makeText(this@MainActivity, output.toString(), Toast.LENGTH_SHORT).show()
Expand All @@ -59,7 +65,13 @@ class MainActivity : ComponentActivity() {
// Try auto-connecting
viewModel.autoConnect()

if (!ConfigurationManager.isNotificationPermissionGranted(this) || !ConfigurationManager.isWirelessDebuggingEnabled(this)) {
// Fetch indicators on first launch and schedule daily background updates
setupIndicatorsUpdates()

if (!ConfigurationManager.isNotificationPermissionGranted(this) || !ConfigurationManager.isWirelessDebuggingEnabled(
this
)
) {
setLacksPermissionsCallback?.invoke(true)
}

Expand All @@ -77,6 +89,40 @@ class MainActivity : ComponentActivity() {
}
}
}

private fun setupIndicatorsUpdates() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

// Kick an immediate, one-time run if nothing has been downloaded yet
val updates = IndicatorsUpdates(filesDir.toPath(), null)
if (updates.latestUpdate == 0L) {
val now = OneTimeWorkRequestBuilder<IndicatorsUpdateWorker>()
.setConstraints(constraints)
.addTag("IndicatorsUpdate") // common tag for querying later if you want
.build()

WorkManager.getInstance(this).enqueueUniqueWork(
"IndicatorsUpdateInitial",
ExistingWorkPolicy.KEEP,
now
)
}

// Schedule daily periodic job (unique)
val periodic = PeriodicWorkRequestBuilder<IndicatorsUpdateWorker>(1, TimeUnit.DAYS)
.setConstraints(constraints)
.addTag("IndicatorsUpdate")
.build()

WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"IndicatorsUpdatePeriodic",
ExistingPeriodicWorkPolicy.KEEP,
periodic
)
Log.i("MainActivity", "Scheduled daily indicator update worker")
}
}

@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.osservatorionessuno.bugbane.analysis

import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import org.osservatorionessuno.libmvt.common.Indicators
import org.osservatorionessuno.libmvt.common.IndicatorsUpdates
import org.osservatorionessuno.libmvt.matcher.IndicatorMatcher
import java.io.File
import java.nio.file.Files
import java.security.MessageDigest
import java.time.Instant
import java.util.UUID

object AcquisitionScanner {
fun scan(context: Context, acquisitionDir: File): File {
val indicatorsDir = IndicatorsUpdates(context.filesDir.toPath(), null).getIndicatorsFolder().toFile()
return scanWithIndicators(acquisitionDir, indicatorsDir)
}

private fun scanWithIndicators(acquisitionDir: File, indicatorsDir: File): File {
val started = Instant.now()
val indicators = Indicators.loadFromDirectory(indicatorsDir)
val matcher = IndicatorMatcher(indicators)

val indicatorsArr = JSONArray()
val indicatorHashes = mutableListOf<String>()
indicatorsDir.listFiles { file -> file.isFile }?.forEach { file ->
val hash = sha256(file)
val obj = JSONObject()
obj.put("file", file.name)
obj.put("sha256", hash)
indicatorsArr.put(obj)
indicatorHashes += hash
}

val results = JSONArray()
val analysisPath = File(acquisitionDir, "analysis").toPath()
acquisitionDir.walkTopDown().forEach { file ->
if (file.isFile && !file.toPath().startsWith(analysisPath)) {
val lines: List<String> = try {
Files.readAllLines(file.toPath())
} catch (_: Exception) {
emptyList()
}
val detections = matcher.matchAllStrings(lines)
if (detections.isNotEmpty()) {
val rel = file.relativeTo(acquisitionDir).path
for (d in detections) {
val obj = JSONObject()
obj.put("file", rel)
obj.put("type", d.type.name)
obj.put("ioc", d.ioc)
obj.put("context", d.context)
results.put(obj)
}
}
}
}

val completed = Instant.now()
val outDir = analysisPath.toFile()
if (!outDir.exists()) outDir.mkdirs()
val uuid = UUID.randomUUID().toString()
val outFile = File(outDir, "${started.toString().replace(':', '-')}.json")
indicatorHashes.sort()
val root = JSONObject()
root.put("uuid", uuid)
root.put("started", started.toString())
root.put("completed", completed.toString())
root.put("indicatorsHash", sha256(indicatorHashes.joinToString("")))
root.put("indicators", indicatorsArr)
root.put("results", results)
outFile.writeText(root.toString(1))
return outFile
}

private fun sha256(file: File): String {
val md = MessageDigest.getInstance("SHA-256")
file.inputStream().use { input ->
val buffer = ByteArray(8192)
while (true) {
val read = input.read(buffer)
if (read < 0) break
md.update(buffer, 0, read)
}
}
return md.digest().joinToString("") { "%02x".format(it) }
}

private fun sha256(data: String): String {
val md = MessageDigest.getInstance("SHA-256")
md.update(data.toByteArray())
return md.digest().joinToString("") { "%02x".format(it) }
}
}

Loading
Loading