Skip to content
Open
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
31 changes: 31 additions & 0 deletions demos/android/MASVS-CODE/MASTG-DEMO-0x36/MASTG-DEMO-0x36.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
platform: android
title: Enforced Immediate Updates with Play Core API detected using semgrep
id: MASTG-DEMO-0x36
code: [kotlin]
test: MASTG-TEST-0x336
---

### Sample

The following code implements immediate in-app updates using the Google Play Core API by calling `startUpdateFlowForResult` with `AppUpdateOptions.newBuilder(1)`.

{{ MastgTest.kt # MastgTest_reversed.java }}

### Steps

Let's run @MASTG-TOOL-0110 rules against the sample code.

{{ ../../../../rules/mastg-android-enforced-updating.yml }}

{{ run.sh }}

### Observation

The output file shows usages of the Google Play Core API enforcing immediate update.

{{ output.txt }}

### Evaluation

The test passes because the app forces users to update the application immediately and does not provide a fallback.
90 changes: 90 additions & 0 deletions demos/android/MASVS-CODE/MASTG-DEMO-0x36/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.owasp.mastestapp

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

const val MASTG_TEXT_TAG = "mastgTestText"

class MainActivity : ComponentActivity() {

private val mastgTest by lazy { MastgTest(applicationContext) }
private lateinit var appUpdateResultLauncher: ActivityResultLauncher<IntentSenderRequest>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

// mastgTest = MastgTest(applicationContext)
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove commented-out code. The initialization is already handled by the lazy delegate on line 26.

Suggested change
// mastgTest = MastgTest(applicationContext)

Copilot uses AI. Check for mistakes.

appUpdateResultLauncher = registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) { result ->
if (result.resultCode != RESULT_OK) {
Log.e(
"MainActivity",
"Update flow was cancelled or failed! Result code: ${result.resultCode}. Re-initiating."
)
// The unused 'this' parameter is now removed.
mastgTest.checkForUpdate(appUpdateResultLauncher)
Comment on lines +43 to +44
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove obsolete comment referring to a removed parameter. The comment no longer provides relevant context.

Copilot uses AI. Check for mistakes.
} else {
Log.d("MainActivity", "Update accepted. The update is now in progress.")
}
}

setContent {
MainScreen(
displayString = "App is running. Checking for mandatory updates...",
onStartClick = {
// The unused 'this' parameter is now removed.
mastgTest.checkForUpdate(appUpdateResultLauncher)
Comment on lines +54 to +55
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove obsolete comment referring to a removed parameter. The comment no longer provides relevant context.

Copilot uses AI. Check for mistakes.
}
)
}

// The unused 'this' parameter is now removed.
mastgTest.checkForUpdate(appUpdateResultLauncher)
Comment on lines +60 to +61
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove obsolete comment referring to a removed parameter. The comment no longer provides relevant context.

Copilot uses AI. Check for mistakes.
}

override fun onResume() {
super.onResume()
// if (::mastgTest.isInitialized) {
// The unused 'this' parameter is now removed.
mastgTest.resumeUpdateIfInProgress(appUpdateResultLauncher)
Comment on lines +66 to +68
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove commented-out initialization check and obsolete parameter comment. Since mastgTest is initialized by lazy delegation, the check is unnecessary.

Copilot uses AI. Check for mistakes.
// }
}
}

@Preview
@Composable
fun MainScreen(
displayString: String = "App is running.",
onStartClick: () -> Unit = {}
) {
BaseScreen(onStartClick = onStartClick) {
Text(
modifier = Modifier
.padding(16.dp)
.testTag(MASTG_TEXT_TAG),
text = displayString,
color = Color.White,
fontSize = 16.sp,
fontFamily = FontFamily.Monospace
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.owasp.mastestapp;

import androidx.compose.foundation.layout.PaddingKt;
import androidx.compose.material3.TextKt;
import androidx.compose.runtime.Composer;
import androidx.compose.runtime.ComposerKt;
import androidx.compose.runtime.RecomposeScopeImplKt;
import androidx.compose.runtime.ScopeUpdateScope;
import androidx.compose.runtime.internal.ComposableLambdaKt;
import androidx.compose.ui.Modifier;
import androidx.compose.ui.graphics.Color;
import androidx.compose.ui.platform.TestTagKt;
import androidx.compose.ui.text.TextLayoutResult;
import androidx.compose.ui.text.TextStyle;
import androidx.compose.ui.text.font.FontFamily;
import androidx.compose.ui.text.font.FontStyle;
import androidx.compose.ui.text.font.FontWeight;
import androidx.compose.ui.text.style.TextAlign;
import androidx.compose.ui.text.style.TextDecoration;
import androidx.compose.ui.unit.Dp;
import androidx.compose.ui.unit.TextUnitKt;
import kotlin.Metadata;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.functions.Function2;

/* compiled from: MainActivity.kt */
@Metadata(d1 = {"\u0000\u0018\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\u001a'\u0010\u0002\u001a\u00020\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u00012\u000e\b\u0002\u0010\u0005\u001a\b\u0012\u0004\u0012\u00020\u00030\u0006H\u0007¢\u0006\u0002\u0010\u0007\"\u000e\u0010\u0000\u001a\u00020\u0001X\u0086T¢\u0006\u0002\n\u0000¨\u0006\b"}, d2 = {"MASTG_TEXT_TAG", "", "MainScreen", "", "displayString", "onStartClick", "Lkotlin/Function0;", "(Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V", "app_debug"}, k = 2, mv = {2, 0, 0}, xi = 48)
/* loaded from: classes3.dex */
public final class MainActivityKt {
public static final String MASTG_TEXT_TAG = "mastgTestText";

/* JADX INFO: Access modifiers changed from: private */
public static final Unit MainScreen$lambda$1(String str, Function0 function0, int i, int i2, Composer composer, int i3) {
MainScreen(str, function0, composer, RecomposeScopeImplKt.updateChangedFlags(i | 1), i2);
return Unit.INSTANCE;
}

public static final void MainScreen(final String displayString, final Function0<Unit> function0, Composer $composer, final int $changed, final int i) {
Composer $composer2 = $composer.startRestartGroup(-958245146);
ComposerKt.sourceInformation($composer2, "C(MainScreen)101@4458L280,101@4418L320:MainActivity.kt#vyvp3i");
int $dirty = $changed;
int i2 = i & 1;
if (i2 != 0) {
$dirty |= 6;
} else if (($changed & 14) == 0) {
$dirty |= $composer2.changed(displayString) ? 4 : 2;
}
int i3 = i & 2;
if (i3 != 0) {
$dirty |= 48;
} else if (($changed & 112) == 0) {
$dirty |= $composer2.changedInstance(function0) ? 32 : 16;
}
if (($dirty & 91) != 18 || !$composer2.getSkipping()) {
if (i2 != 0) {
displayString = "App is running.";
}
if (i3 != 0) {
function0 = new Function0() { // from class: org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda0
@Override // kotlin.jvm.functions.Function0
public final Object invoke() {
return Unit.INSTANCE;
}
};
}
BaseScreenKt.BaseScreen(function0, ComposableLambdaKt.rememberComposableLambda(891526107, true, new Function2<Composer, Integer, Unit>() { // from class: org.owasp.mastestapp.MainActivityKt.MainScreen.2
@Override // kotlin.jvm.functions.Function2
public /* bridge */ /* synthetic */ Unit invoke(Composer composer, Integer num) {
invoke(composer, num.intValue());
return Unit.INSTANCE;
}

public final void invoke(Composer $composer3, int $changed2) {
ComposerKt.sourceInformation($composer3, "C102@4468L264:MainActivity.kt#vyvp3i");
if (($changed2 & 11) != 2 || !$composer3.getSkipping()) {
TextKt.m2715Text4IGK_g(displayString, TestTagKt.testTag(PaddingKt.m681padding3ABfNKs(Modifier.INSTANCE, Dp.m6664constructorimpl(16)), MainActivityKt.MASTG_TEXT_TAG), Color.INSTANCE.m4219getWhite0d7_KjU(), TextUnitKt.getSp(16), (FontStyle) null, (FontWeight) null, (FontFamily) FontFamily.INSTANCE.getMonospace(), 0L, (TextDecoration) null, (TextAlign) null, 0L, 0, false, 0, 0, (Function1<? super TextLayoutResult, Unit>) null, (TextStyle) null, $composer3, 3504, 0, 130992);
return;
}
$composer3.skipToGroupEnd();
}
}, $composer2, 54), $composer2, (($dirty >> 3) & 14) | 48, 0);
} else {
$composer2.skipToGroupEnd();
}
ScopeUpdateScope scopeUpdateScopeEndRestartGroup = $composer2.endRestartGroup();
if (scopeUpdateScopeEndRestartGroup != null) {
scopeUpdateScopeEndRestartGroup.updateScope(new Function2() { // from class: org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda1
@Override // kotlin.jvm.functions.Function2
public final Object invoke(Object obj, Object obj2) {
return MainActivityKt.MainScreen$lambda$1(displayString, function0, $changed, i, (Composer) obj, ((Integer) obj2).intValue());
}
});
}
}
}
62 changes: 62 additions & 0 deletions demos/android/MASVS-CODE/MASTG-DEMO-0x36/MastgTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.owasp.mastestapp

import android.content.Context
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.UpdateAvailability

class MastgTest(context: Context) {

private val appUpdateManager: AppUpdateManager = AppUpdateManagerFactory.create(context)

/**
* Checks if an IMMEDIATE update is available on the Play Store.
*/
fun checkForUpdate(
appUpdateResultLauncher: ActivityResultLauncher<IntentSenderRequest>
) {
Log.d("MastgTest", "Checking for an update...")
val appUpdateInfoTask = appUpdateManager.appUpdateInfo

appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
val isUpdateAvailable = appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
val isImmediateUpdateAllowed = appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)

if (isUpdateAvailable && isImmediateUpdateAllowed) {
Log.d("MastgTest", "Immediate update available. Starting flow.")
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
appUpdateResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
)
} else {
Log.d("MastgTest", "No immediate update available.")
}
}.addOnFailureListener { e ->
Log.e("MastgTest", "Failed to check for updates.", e)
}
}

/**
* Resumes an update that is already in progress. This is critical for onResume().
*/
fun resumeUpdateIfInProgress(
appUpdateResultLauncher: ActivityResultLauncher<IntentSenderRequest>
) {
appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
Log.d("MastgTest", "Resuming in-progress update.")
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
appUpdateResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
)
}
}
}
}
Loading
Loading