Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1df00fe7 authored by Liz Prucka's avatar Liz Prucka
Browse files

[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
parent e945ea64
Loading
Loading
Loading
Loading
+11 −2
Original line number Diff line number Diff line
@@ -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"
+18 −0
Original line number Diff line number Diff line
<?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>
+223 −1
Original line number Diff line number Diff line
@@ -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;