diff options
| author | 2024-11-03 15:28:16 +0000 | |
|---|---|---|
| committer | 2024-11-22 18:48:36 +0000 | |
| commit | 1df00fe78b3e0f6230f2ea7d554c76d545a615ef (patch) | |
| tree | 83184fc75c47d38ec91104d199cd94d61df63689 | |
| parent | e945ea6404df8869e2ed9cf0a5c556b62b2a6109 (diff) | |
[ID] Add tests of data sources
Registers the ForensicAdminReceiver as DeviceOwner to
receive network logging callbacks and authenticate security
logging.
SecurityLogging test: generates a security event, and ensures
the data reaches the BackupTransportConnection.
NetworkLogging test: generates a network event, and ensures
the data reaches the BackupTransportConnection.
Bug: 365994454
Test: atest IntrusionDetectionServiceTests
Flag: android.security.afl_api
Ignore-AOSP-First: security feature
Change-Id: I7601bc73b1f6b76225314baf5edeeafb76f4c6f9
3 files changed, 252 insertions, 3 deletions
diff --git a/services/tests/security/intrusiondetection/AndroidManifest.xml b/services/tests/security/intrusiondetection/AndroidManifest.xml index f388e7ea8590..39e41cddb662 100644 --- a/services/tests/security/intrusiondetection/AndroidManifest.xml +++ b/services/tests/security/intrusiondetection/AndroidManifest.xml @@ -18,10 +18,19 @@ package="com.android.server.security.intrusiondetection.tests"> <uses-permission android:name="android.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING" /> - <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" /> + <uses-permission android:name="android.permission.INTERNET"/> - <application android:testOnly="true"> + <application android:testOnly="true" android:debuggable="true" android:usesCleartextTraffic="true"> <uses-library android:name="android.test.runner"/> + <receiver android:name="com.android.server.security.intrusiondetection.IntrusionDetectionAdminReceiver" + android:permission="android.permission.BIND_DEVICE_ADMIN" + android:exported="true"> + <meta-data android:name="android.app.device_admin" + android:resource="@xml/device_admin"/> + <intent-filter> + <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/> + </intent-filter> + </receiver> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/services/tests/security/intrusiondetection/res/xml/device_admin.xml b/services/tests/security/intrusiondetection/res/xml/device_admin.xml new file mode 100644 index 000000000000..f8cd8f0b9b44 --- /dev/null +++ b/services/tests/security/intrusiondetection/res/xml/device_admin.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<device-admin xmlns:android="http://schemas.android.com/apk/res/android"> +</device-admin> diff --git a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java index bc854cf6488b..c185ad5ffd61 100644 --- a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java +++ b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java @@ -16,14 +16,19 @@ package com.android.server.security.intrusiondetection; +import static android.Manifest.permission.INTERNET; import static android.Manifest.permission.MANAGE_INTRUSION_DETECTION_STATE; import static android.Manifest.permission.READ_INTRUSION_DETECTION_STATE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; @@ -33,7 +38,9 @@ import static org.mockito.Mockito.verify; import android.annotation.SuppressLint; import android.app.admin.ConnectEvent; import android.app.admin.DnsEvent; +import android.app.admin.SecurityLog; import android.app.admin.SecurityLog.SecurityEvent; +import android.content.ComponentName; import android.content.Context; import android.os.Looper; import android.os.PermissionEnforcer; @@ -43,19 +50,44 @@ import android.os.test.TestLooper; import android.security.intrusiondetection.IIntrusionDetectionServiceCommandCallback; import android.security.intrusiondetection.IIntrusionDetectionServiceStateCallback; import android.security.intrusiondetection.IntrusionDetectionEvent; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; import androidx.test.core.app.ApplicationProvider; +import com.android.bedstead.harrier.BedsteadJUnit4; +import com.android.bedstead.harrier.annotations.AfterClass; +import com.android.bedstead.harrier.annotations.BeforeClass; +import com.android.bedstead.multiuser.annotations.RequireRunOnSystemUser; +import com.android.bedstead.nene.TestApis; +import com.android.bedstead.nene.devicepolicy.DeviceOwner; +import com.android.bedstead.nene.exceptions.NeneException; +import com.android.bedstead.permissions.CommonPermissions; +import com.android.bedstead.permissions.PermissionContext; +import com.android.bedstead.permissions.annotations.EnsureHasPermission; import com.android.server.ServiceThread; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; - +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +@RunWith(BedsteadJUnit4.class) public class IntrusionDetectionServiceTest { private static final int STATE_UNKNOWN = IIntrusionDetectionServiceStateCallback.State.UNKNOWN; @@ -73,6 +105,8 @@ public class IntrusionDetectionServiceTest { private static final int ERROR_DATA_SOURCE_UNAVAILABLE = IIntrusionDetectionServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE; + private static DeviceOwner sDeviceOwner; + private Context mContext; private IntrusionDetectionEventTransportConnection mIntrusionDetectionEventTransportConnection; private DataAggregator mDataAggregator; @@ -83,6 +117,28 @@ public class IntrusionDetectionServiceTest { private Looper mLooperOfDataAggregator; private FakePermissionEnforcer mPermissionEnforcer; + @BeforeClass + public static void setDeviceOwner() { + ComponentName admin = + new ComponentName( + ApplicationProvider.getApplicationContext(), + IntrusionDetectionAdminReceiver.class); + try { + sDeviceOwner = TestApis.devicePolicy().setDeviceOwner(admin); + } catch (NeneException e) { + fail("Failed to set device owner " + admin.flattenToString() + ": " + e); + } + } + + @AfterClass + public static void removeDeviceOwner() { + try { + sDeviceOwner.remove(); + } catch (NeneException e) { + fail("Failed to remove device owner : " + e); + } + } + @SuppressLint("VisibleForTests") @Before public void setUp() { @@ -343,6 +399,172 @@ public class IntrusionDetectionServiceTest { assertNotNull(receivedEvents.get(2).getDnsEvent()); } + @Test + @RequireRunOnSystemUser + public void testDataSources_Initialize_HasDeviceOwner() throws Exception { + NetworkLogSource networkLogSource = new NetworkLogSource(mContext, mDataAggregator); + SecurityLogSource securityLogSource = new SecurityLogSource(mContext, mDataAggregator); + + assertTrue(networkLogSource.initialize()); + assertTrue(securityLogSource.initialize()); + } + + @Test + @RequireRunOnSystemUser + public void testDataSources_Initialize_NoDeviceOwner() throws Exception { + NetworkLogSource networkLogSource = new NetworkLogSource(mContext, mDataAggregator); + SecurityLogSource securityLogSource = new SecurityLogSource(mContext, mDataAggregator); + ComponentName admin = sDeviceOwner.componentName(); + + try { + sDeviceOwner.remove(); + assertFalse(networkLogSource.initialize()); + assertFalse(securityLogSource.initialize()); + } finally { + sDeviceOwner = TestApis.devicePolicy().setDeviceOwner(admin); + } + } + + @Test + @RequireRunOnSystemUser + @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) + public void testDataAggregator_AddSecurityEvent() throws Exception { + mIntrusionDetectionService.setState(STATE_ENABLED); + ServiceThread mockThread = spy(ServiceThread.class); + mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread); + assertTrue(mDataAggregator.initialize()); + + // SecurityLogging generates a number of events and callbacks, so create a latch to wait for + // the given event. + String eventString = this.getClass().getName() + ".testSecurityEvent"; + + final CountDownLatch latch = new CountDownLatch(1); + // TODO: Replace this mock when the IntrusionDetectionEventTransportConnection is ready. + doAnswer( + new Answer<Boolean>() { + @Override + public Boolean answer(InvocationOnMock input) { + List<IntrusionDetectionEvent> receivedEvents = + (List<IntrusionDetectionEvent>) input.getArguments()[0]; + for (IntrusionDetectionEvent event : receivedEvents) { + if (event.getType() == IntrusionDetectionEvent.SECURITY_EVENT) { + SecurityEvent securityEvent = event.getSecurityEvent(); + Object[] eventData = (Object[]) securityEvent.getData(); + if (securityEvent.getTag() == SecurityLog.TAG_KEY_GENERATED + && eventData[1].equals(eventString)) { + latch.countDown(); + } + } + } + return true; + } + }) + .when(mIntrusionDetectionEventTransportConnection).addData(any()); + mDataAggregator.enable(); + + // Generate the security event. + generateSecurityEvent(eventString); + TestApis.devicePolicy().forceSecurityLogs(); + + // Verify the event is received. + mTestLooper.startAutoDispatch(); + assertTrue(latch.await(1, TimeUnit.SECONDS)); + mTestLooper.stopAutoDispatch(); + + mDataAggregator.disable(); + } + + @Test + @RequireRunOnSystemUser + @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) + public void testDataAggregator_AddNetworkEvent() throws Exception { + mIntrusionDetectionService.setState(STATE_ENABLED); + ServiceThread mockThread = spy(ServiceThread.class); + mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread); + assertTrue(mDataAggregator.initialize()); + + // Network logging may log multiple and callbacks, so create a latch to wait for + // the given event. + // eventServer must be a valid domain to generate a network log event. + String eventServer = "google.com"; + final CountDownLatch latch = new CountDownLatch(1); + // TODO: Replace this mock when the IntrusionDetectionEventTransportConnection is ready. + doAnswer( + new Answer<Boolean>() { + @Override + public Boolean answer(InvocationOnMock input) { + List<IntrusionDetectionEvent> receivedEvents = + (List<IntrusionDetectionEvent>) input.getArguments()[0]; + for (IntrusionDetectionEvent event : receivedEvents) { + if (event.getType() + == IntrusionDetectionEvent.NETWORK_EVENT_DNS) { + DnsEvent dnsEvent = event.getDnsEvent(); + if (dnsEvent.getHostname().equals(eventServer)) { + latch.countDown(); + } + } + } + return true; + } + }) + .when(mIntrusionDetectionEventTransportConnection).addData(any()); + mDataAggregator.enable(); + + // Generate the network event. + generateNetworkEvent(eventServer); + TestApis.devicePolicy().forceNetworkLogs(); + + // Verify the event is received. + mTestLooper.startAutoDispatch(); + assertTrue(latch.await(1, TimeUnit.SECONDS)); + mTestLooper.stopAutoDispatch(); + + mDataAggregator.disable(); + } + + /** Emits a given string into security log (if enabled). */ + private void generateSecurityEvent(String eventString) + throws IllegalArgumentException, GeneralSecurityException, IOException { + if (eventString == null || eventString.isEmpty()) { + throw new IllegalArgumentException( + "Error generating security event: eventString must not be empty"); + } + + final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore"); + keyGen.initialize( + new KeyGenParameterSpec.Builder(eventString, KeyProperties.PURPOSE_SIGN).build()); + // Emit key generation event. + final KeyPair keyPair = keyGen.generateKeyPair(); + assertNotNull(keyPair); + + final KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); + ks.load(null); + // Emit key destruction event. + ks.deleteEntry(eventString); + } + + /** Emits a given string into network log (if enabled). */ + private void generateNetworkEvent(String server) throws IllegalArgumentException, IOException { + if (server == null || server.isEmpty()) { + throw new IllegalArgumentException( + "Error generating network event: server must not be empty"); + } + + HttpURLConnection urlConnection = null; + int connectionTimeoutMS = 2_000; + try (PermissionContext p = TestApis.permissions().withPermission(INTERNET)) { + final URL url = new URL("http://" + server); + urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setConnectTimeout(connectionTimeoutMS); + urlConnection.setReadTimeout(connectionTimeoutMS); + urlConnection.getResponseCode(); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + private class MockInjector implements IntrusionDetectionService.Injector { private final Context mContext; |