diff options
| author | 2023-11-10 13:51:48 +0000 | |
|---|---|---|
| committer | 2024-01-25 12:44:41 +0000 | |
| commit | dee33192b3d3e974ce79793ab5e79aa49d0047fb (patch) | |
| tree | 65353487176d04ee30fecdf9912ca0476aeee151 | |
| parent | b363ed1d64118a286f09c393fd7dcaefd7fbd6c2 (diff) | |
Add SelinuxAuditLogs collection to SystemServer
Bug: 295861450
Test: atest SelinuxAuditLogsCollectorTest
Change-Id: I2e0b6b537c513bd4f9580c414e3f4a9b1f23df62
10 files changed, 1566 insertions, 0 deletions
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 070e2cb0dc5e..723eb70a00d9 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8415,6 +8415,10 @@ android:permission="android.permission.BIND_JOB_SERVICE"> </service> + <service android:name="com.android.server.selinux.SelinuxAuditLogsService" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + <service android:name="com.android.server.compos.IsolatedCompilationJobService" android:permission="android.permission.BIND_JOB_SERVICE"> </service> diff --git a/services/core/java/com/android/server/selinux/QuotaLimiter.java b/services/core/java/com/android/server/selinux/QuotaLimiter.java new file mode 100644 index 000000000000..e89ddfd2627c --- /dev/null +++ b/services/core/java/com/android/server/selinux/QuotaLimiter.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.server.selinux; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.Clock; + +import java.time.Duration; +import java.time.Instant; + +/** + * A QuotaLimiter allows to define a maximum number of Atom pushes within a specific time window. + * + * <p>The limiter divides the time line in windows of a fixed size. Every time a new permit is + * requested, the limiter checks whether the previous request was in the same time window as the + * current one. If the two windows are the same, it grants a permit only if the number of permits + * granted within the window does not exceed the quota. If the two windows are different, it resets + * the quota. + */ +public class QuotaLimiter { + + private final Clock mClock; + private final Duration mWindowSize; + private final int mMaxPermits; + + private long mCurrentWindow = 0; + private int mPermitsGranted = 0; + + @VisibleForTesting + QuotaLimiter(Clock clock, Duration windowSize, int maxPermits) { + mClock = clock; + mWindowSize = windowSize; + mMaxPermits = maxPermits; + } + + public QuotaLimiter(Duration windowSize, int maxPermits) { + this(Clock.SYSTEM_CLOCK, windowSize, maxPermits); + } + + public QuotaLimiter(int maxPermitsPerDay) { + this(Clock.SYSTEM_CLOCK, Duration.ofDays(1), maxPermitsPerDay); + } + + /** + * Acquires a permit if there is one available in the current time window. + * + * @return true if a permit was acquired. + */ + boolean acquire() { + long nowWindow = + Duration.between(Instant.EPOCH, Instant.ofEpochMilli(mClock.currentTimeMillis())) + .dividedBy(mWindowSize); + if (nowWindow > mCurrentWindow) { + mCurrentWindow = nowWindow; + mPermitsGranted = 0; + } + + if (mPermitsGranted < mMaxPermits) { + mPermitsGranted++; + return true; + } + + return false; + } +} diff --git a/services/core/java/com/android/server/selinux/RateLimiter.java b/services/core/java/com/android/server/selinux/RateLimiter.java new file mode 100644 index 000000000000..599b8409cbc3 --- /dev/null +++ b/services/core/java/com/android/server/selinux/RateLimiter.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.server.selinux; + +import android.os.SystemClock; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.Clock; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +/** + * Rate limiter to ensure Atoms are pushed only within the allowed QPS window. This class is not + * thread-safe. + * + * <p>The rate limiter is smoothed, meaning that a rate limiter allowing X permits per second (or X + * QPS) will grant permits at a ratio of one every 1/X seconds. + */ +public final class RateLimiter { + + private Instant mNextPermit = Instant.EPOCH; + + private final Clock mClock; + private final Duration mWindow; + + @VisibleForTesting + RateLimiter(Clock clock, Duration window) { + mClock = clock; + // Truncating because the system clock does not support units smaller than milliseconds. + mWindow = window; + } + + /** + * Create a rate limiter generating one permit every {@code window} of time, using the {@link + * Clock.SYSTEM_CLOCK}. + */ + public RateLimiter(Duration window) { + this(Clock.SYSTEM_CLOCK, window); + } + + /** + * Acquire a permit if allowed by the rate limiter. If not, wait until a permit becomes + * available. + */ + public void acquire() { + Instant now = Instant.ofEpochMilli(mClock.currentTimeMillis()); + + if (mNextPermit.isAfter(now)) { // Sleep until we can acquire. + SystemClock.sleep(ChronoUnit.MILLIS.between(now, mNextPermit)); + mNextPermit = mNextPermit.plus(mWindow); + } else { + mNextPermit = now.plus(mWindow); + } + } + + /** + * Try to acquire a permit if allowed by the rate limiter. Non-blocking. + * + * @return true if a permit was acquired. Otherwise, return false. + */ + public boolean tryAcquire() { + final Instant now = Instant.ofEpochMilli(mClock.currentTimeMillis()); + + if (mNextPermit.isAfter(now)) { + return false; + } + mNextPermit = now.plus(mWindow); + return true; + } +} diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java new file mode 100644 index 000000000000..8d8d5960038e --- /dev/null +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.server.selinux; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** Builder for SelinuxAuditLogs. */ +class SelinuxAuditLogBuilder { + + // Currently logs collection is hardcoded for the sdk_sandbox_audit. + private static final String SDK_SANDBOX_AUDIT = "sdk_sandbox_audit"; + static final Matcher SCONTEXT_MATCHER = + Pattern.compile( + "u:r:(?<stype>" + + SDK_SANDBOX_AUDIT + + "):s0(:c)?(?<scategories>((,c)?\\d+)+)*") + .matcher(""); + + static final Matcher TCONTEXT_MATCHER = + Pattern.compile("u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*") + .matcher(""); + + static final Matcher PATH_MATCHER = + Pattern.compile("\"(?<path>/\\w+(/\\w+)?)(/\\w+)*\"").matcher(""); + + private Iterator<String> mTokens; + private final SelinuxAuditLog mAuditLog = new SelinuxAuditLog(); + + void reset(String denialString) { + mTokens = + Arrays.asList( + Optional.ofNullable(denialString) + .map(s -> s.split("\\s+|=")) + .orElse(new String[0])) + .iterator(); + mAuditLog.reset(); + } + + SelinuxAuditLog build() { + while (mTokens.hasNext()) { + final String token = mTokens.next(); + + switch (token) { + case "granted": + mAuditLog.mGranted = true; + break; + case "denied": + mAuditLog.mGranted = false; + break; + case "{": + Stream.Builder<String> permissionsStream = Stream.builder(); + boolean closed = false; + while (!closed && mTokens.hasNext()) { + String permission = mTokens.next(); + if ("}".equals(permission)) { + closed = true; + } else { + permissionsStream.add(permission); + } + } + if (!closed) { + return null; + } + mAuditLog.mPermissions = permissionsStream.build().toArray(String[]::new); + break; + case "scontext": + if (!nextTokenMatches(SCONTEXT_MATCHER)) { + return null; + } + mAuditLog.mSType = SCONTEXT_MATCHER.group("stype"); + mAuditLog.mSCategories = toCategories(SCONTEXT_MATCHER.group("scategories")); + break; + case "tcontext": + if (!nextTokenMatches(TCONTEXT_MATCHER)) { + return null; + } + mAuditLog.mTType = TCONTEXT_MATCHER.group("ttype"); + mAuditLog.mTCategories = toCategories(TCONTEXT_MATCHER.group("tcategories")); + break; + case "tclass": + if (!mTokens.hasNext()) { + return null; + } + mAuditLog.mTClass = mTokens.next(); + break; + case "path": + if (nextTokenMatches(PATH_MATCHER)) { + mAuditLog.mPath = PATH_MATCHER.group("path"); + } + break; + case "permissive": + if (!mTokens.hasNext()) { + return null; + } + mAuditLog.mPermissive = "1".equals(mTokens.next()); + break; + default: + break; + } + } + return mAuditLog; + } + + boolean nextTokenMatches(Matcher matcher) { + return mTokens.hasNext() && matcher.reset(mTokens.next()).matches(); + } + + static int[] toCategories(String categories) { + return categories == null + ? null + : Arrays.stream(categories.split(",c")).mapToInt(Integer::parseInt).toArray(); + } + + static class SelinuxAuditLog { + boolean mGranted = false; + String[] mPermissions = null; + String mSType = null; + int[] mSCategories = null; + String mTType = null; + int[] mTCategories = null; + String mTClass = null; + String mPath = null; + boolean mPermissive = false; + + private void reset() { + mGranted = false; + mPermissions = null; + mSType = null; + mSCategories = null; + mTType = null; + mTCategories = null; + mTClass = null; + mPath = null; + mPermissive = false; + } + } +} diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java new file mode 100644 index 000000000000..0219645bee38 --- /dev/null +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.server.selinux; + +import android.util.EventLog; +import android.util.EventLog.Event; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; +import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Class in charge of collecting SELinux audit logs and push the SELinux atoms. */ +class SelinuxAuditLogsCollector { + + private static final String TAG = "SelinuxAuditLogs"; + + private static final String SELINUX_PATTERN = "^.*\\bavc:\\s+(?<denial>.*)$"; + + @VisibleForTesting + static final Matcher SELINUX_MATCHER = Pattern.compile(SELINUX_PATTERN).matcher(""); + + private final RateLimiter mRateLimiter; + private final QuotaLimiter mQuotaLimiter; + + @VisibleForTesting Instant mLastWrite = Instant.MIN; + + final AtomicBoolean mStopRequested = new AtomicBoolean(false); + + SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { + mRateLimiter = rateLimiter; + mQuotaLimiter = quotaLimiter; + } + + /** + * Collect and push SELinux audit logs for the provided {@code tagCode}. + * + * @return true if the job was completed. If the job was interrupted, return false. + */ + boolean collect(int tagCode) { + Queue<Event> logLines = new ArrayDeque<>(); + Instant latestTimestamp = collectLogLines(tagCode, logLines); + + boolean quotaExceeded = writeAuditLogs(logLines); + if (quotaExceeded) { + Log.w(TAG, "Too many SELinux logs in the queue, I am giving up."); + mLastWrite = latestTimestamp; // next run we will ignore all these logs. + logLines.clear(); + } + + return logLines.isEmpty(); + } + + private Instant collectLogLines(int tagCode, Queue<Event> logLines) { + List<Event> events = new ArrayList<>(); + try { + EventLog.readEvents(new int[] {tagCode}, events); + } catch (IOException e) { + Log.e(TAG, "Error reading event logs", e); + } + + Instant latestTimestamp = mLastWrite; + for (Event event : events) { + Instant eventTime = Instant.ofEpochSecond(0, event.getTimeNanos()); + if (eventTime.isAfter(latestTimestamp)) { + latestTimestamp = eventTime; + } + if (eventTime.isBefore(mLastWrite)) { + continue; + } + Object eventData = event.getData(); + if (!(eventData instanceof String)) { + continue; + } + logLines.add(event); + } + return latestTimestamp; + } + + private boolean writeAuditLogs(Queue<Event> logLines) { + final SelinuxAuditLogBuilder auditLogBuilder = new SelinuxAuditLogBuilder(); + + while (!mStopRequested.get() && !logLines.isEmpty()) { + Event event = logLines.poll(); + String logLine = (String) event.getData(); + Instant logTime = Instant.ofEpochSecond(0, event.getTimeNanos()); + if (!SELINUX_MATCHER.reset(logLine).matches()) { + continue; + } + + auditLogBuilder.reset(SELINUX_MATCHER.group("denial")); + final SelinuxAuditLog auditLog = auditLogBuilder.build(); + if (auditLog == null) { + continue; + } + + if (!mQuotaLimiter.acquire()) { + return true; + } + mRateLimiter.acquire(); + + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + auditLog.mGranted, + auditLog.mPermissions, + auditLog.mSType, + auditLog.mSCategories, + auditLog.mTType, + auditLog.mTCategories, + auditLog.mTClass, + auditLog.mPath, + auditLog.mPermissive); + + if (logTime.isAfter(mLastWrite)) { + mLastWrite = logTime; + } + } + + return false; + } +} diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java new file mode 100644 index 000000000000..8a661bcc13af --- /dev/null +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.server.selinux; + +import static com.android.sdksandbox.flags.Flags.selinuxSdkSandboxAudit; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.util.EventLog; +import android.util.Log; + +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Scheduled jobs related to logging of SELinux denials and audits. The job runs daily on idle + * devices. + */ +public class SelinuxAuditLogsService extends JobService { + + private static final String TAG = "SelinuxAuditLogs"; + private static final String SELINUX_AUDIT_NAMESPACE = "SelinuxAuditLogsNamespace"; + + static final int AUDITD_TAG_CODE = EventLog.getTagCode("auditd"); + + private static final int SELINUX_AUDIT_JOB_ID = 25327386; + private static final JobInfo SELINUX_AUDIT_JOB = + new JobInfo.Builder( + SELINUX_AUDIT_JOB_ID, + new ComponentName("android", SelinuxAuditLogsService.class.getName())) + .setPeriodic(TimeUnit.DAYS.toMillis(1)) + .setRequiresDeviceIdle(true) + .setRequiresCharging(true) + .setRequiresBatteryNotLow(true) + .build(); + + private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor(); + private static final AtomicReference<Boolean> IS_RUNNING = new AtomicReference<>(false); + + // Audit logging is subject to both rate and quota limiting. We can only push one atom every 10 + // milliseconds, and no more than 50K atoms can be pushed each day. + private static final SelinuxAuditLogsCollector AUDIT_LOGS_COLLECTOR = + new SelinuxAuditLogsCollector( + new RateLimiter(/* window= */ Duration.ofMillis(10)), + new QuotaLimiter(/* maxPermitsPerDay= */ 50000)); + + /** Schedule jobs with the {@link JobScheduler}. */ + public static void schedule(Context context) { + if (!selinuxSdkSandboxAudit()) { + Log.d(TAG, "SelinuxAuditLogsService not enabled"); + return; + } + + if (AUDITD_TAG_CODE == -1) { + Log.e(TAG, "auditd is not a registered tag on this system"); + return; + } + + if (context.getSystemService(JobScheduler.class) + .forNamespace(SELINUX_AUDIT_NAMESPACE) + .schedule(SELINUX_AUDIT_JOB) + == JobScheduler.RESULT_FAILURE) { + Log.e(TAG, "SelinuxAuditLogsService could not be started."); + } + } + + @Override + public boolean onStartJob(JobParameters params) { + if (params.getJobId() != SELINUX_AUDIT_JOB_ID) { + Log.e(TAG, "The job id does not match the expected selinux job id."); + return false; + } + + AUDIT_LOGS_COLLECTOR.mStopRequested.set(false); + IS_RUNNING.set(true); + EXECUTOR_SERVICE.execute(new LogsCollectorJob(this, params)); + + return true; // the job is running + } + + @Override + public boolean onStopJob(JobParameters params) { + if (params.getJobId() != SELINUX_AUDIT_JOB_ID) { + return false; + } + + AUDIT_LOGS_COLLECTOR.mStopRequested.set(true); + return IS_RUNNING.get(); + } + + private static class LogsCollectorJob implements Runnable { + private final JobService mAuditLogService; + private final JobParameters mParams; + + LogsCollectorJob(JobService auditLogService, JobParameters params) { + mAuditLogService = auditLogService; + mParams = params; + } + + @Override + public void run() { + IS_RUNNING.updateAndGet( + isRunning -> { + boolean done = AUDIT_LOGS_COLLECTOR.collect(AUDITD_TAG_CODE); + if (done) { + mAuditLogService.jobFinished(mParams, /* wantsReschedule= */ false); + } + return !done; + }); + } + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 86ad49458c48..e3f2c530f14e 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -203,6 +203,7 @@ import com.android.server.security.FileIntegrityService; import com.android.server.security.KeyAttestationApplicationIdProviderService; import com.android.server.security.KeyChainSystemService; import com.android.server.security.rkp.RemoteProvisioningService; +import com.android.server.selinux.SelinuxAuditLogsService; import com.android.server.sensorprivacy.SensorPrivacyService; import com.android.server.sensors.SensorService; import com.android.server.signedconfig.SignedConfigService; @@ -2609,6 +2610,14 @@ public final class SystemServer implements Dumpable { t.traceEnd(); } + t.traceBegin("StartSelinuxAuditLogsService"); + try { + SelinuxAuditLogsService.schedule(context); + } catch (Throwable e) { + reportWtf("starting SelinuxAuditLogsService", e); + } + t.traceEnd(); + // LauncherAppsService uses ShortcutService. t.traceBegin("StartShortcutServiceLifecycle"); mSystemServiceManager.startService(ShortcutService.Lifecycle.class); diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java new file mode 100644 index 000000000000..01c7fbe5bfe9 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.server.selinux; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.os.Clock; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +@RunWith(AndroidJUnit4.class) +public class RateLimiterTest { + + private final MockClock mMockClock = new MockClock(); + + @Test + public void testRateLimiter_1QPS() { + RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofSeconds(1)); + + // First acquire is granted. + assertThat(rateLimiter.tryAcquire()).isTrue(); + // Next acquire is negated because it's too soon. + assertThat(rateLimiter.tryAcquire()).isFalse(); + // Wait >=1 seconds. + mMockClock.currentTimeMillis += Duration.ofSeconds(1).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + } + + @Test + public void testRateLimiter_3QPS() { + RateLimiter rateLimiter = + new RateLimiter( + mMockClock, + Duration.ofSeconds(1).dividedBy(3).truncatedTo(ChronoUnit.MILLIS)); + + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(2).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(3).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(4).toMillis(); + assertThat(rateLimiter.tryAcquire()).isFalse(); + } + + @Test + public void testRateLimiter_infiniteQPS() { + RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofMillis(0)); + + // so many permits. + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + + mMockClock.currentTimeMillis += Duration.ofSeconds(10).toMillis(); + // still so many permits. + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + + mMockClock.currentTimeMillis += Duration.ofDays(-10).toMillis(); + // only going backwards in time you will stop the permits. + assertThat(rateLimiter.tryAcquire()).isFalse(); + assertThat(rateLimiter.tryAcquire()).isFalse(); + assertThat(rateLimiter.tryAcquire()).isFalse(); + } + + @Test + public void testRateLimiter_negativeQPS() { + RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofMillis(-10)); + + // Negative QPS is effectively turning of the rate limiter. + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1000).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + } + + private static final class MockClock extends Clock { + + public long currentTimeMillis = 0; + + @Override + public long currentTimeMillis() { + return currentTimeMillis; + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java new file mode 100644 index 000000000000..b36c9bdaf456 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.server.selinux; + +import static com.android.server.selinux.SelinuxAuditLogBuilder.PATH_MATCHER; +import static com.android.server.selinux.SelinuxAuditLogBuilder.SCONTEXT_MATCHER; +import static com.android.server.selinux.SelinuxAuditLogBuilder.TCONTEXT_MATCHER; +import static com.android.server.selinux.SelinuxAuditLogBuilder.toCategories; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SelinuxAuditLogsBuilderTest { + + private final SelinuxAuditLogBuilder mAuditLogBuilder = new SelinuxAuditLogBuilder(); + + @Test + public void testMatcher_scontext() { + assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0").matches()).isTrue(); + assertThat(SCONTEXT_MATCHER.group("stype")).isEqualTo("sdk_sandbox_audit"); + assertThat(SCONTEXT_MATCHER.group("scategories")).isNull(); + + assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:c123,c456").matches()).isTrue(); + assertThat(SCONTEXT_MATCHER.group("stype")).isEqualTo("sdk_sandbox_audit"); + assertThat(toCategories(SCONTEXT_MATCHER.group("scategories"))) + .isEqualTo(new int[] {123, 456}); + + assertThat(SCONTEXT_MATCHER.reset("u:r:not_sdk_sandbox:s0").matches()).isFalse(); + assertThat(SCONTEXT_MATCHER.reset("u:object_r:sdk_sandbox_audit:s0").matches()).isFalse(); + assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:p123").matches()).isFalse(); + } + + @Test + public void testMatcher_tcontext() { + assertThat(TCONTEXT_MATCHER.reset("u:object_r:target_type:s0").matches()).isTrue(); + assertThat(TCONTEXT_MATCHER.group("ttype")).isEqualTo("target_type"); + assertThat(TCONTEXT_MATCHER.group("tcategories")).isNull(); + + assertThat(TCONTEXT_MATCHER.reset("u:object_r:target_type2:s0:c666").matches()).isTrue(); + assertThat(TCONTEXT_MATCHER.group("ttype")).isEqualTo("target_type2"); + assertThat(toCategories(TCONTEXT_MATCHER.group("tcategories"))).isEqualTo(new int[] {666}); + + assertThat(TCONTEXT_MATCHER.reset("u:r:target_type:s0").matches()).isFalse(); + assertThat(TCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:x456").matches()).isFalse(); + } + + @Test + public void testMatcher_path() { + assertThat(PATH_MATCHER.reset("\"/data\"").matches()).isTrue(); + assertThat(PATH_MATCHER.group("path")).isEqualTo("/data"); + assertThat(PATH_MATCHER.reset("\"/data/local\"").matches()).isTrue(); + assertThat(PATH_MATCHER.group("path")).isEqualTo("/data/local"); + assertThat(PATH_MATCHER.reset("\"/data/local/tmp\"").matches()).isTrue(); + assertThat(PATH_MATCHER.group("path")).isEqualTo("/data/local"); + + assertThat(PATH_MATCHER.reset("\"/data/local").matches()).isFalse(); + assertThat(PATH_MATCHER.reset("\"_data_local\"").matches()).isFalse(); + } + + @Test + public void testSelinuxAuditLogsBuilder_noOptionals() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 tcontext=u:object_r:t:s0" + + " tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), true, new String[] {"p"}, "sdk_sandbox_audit", "t", "c"); + + mAuditLogBuilder.reset( + "tclass=c2 granted { p2 } tcontext=u:object_r:t2:s0" + + " scontext=u:r:sdk_sandbox_audit:s0"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p2"}, + "sdk_sandbox_audit", + "t2", + "c2"); + } + + @Test + public void testSelinuxAuditLogsBuilder_withCategories() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0:c123" + + " tcontext=u:object_r:t:s0:c456,c666 tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + new int[] {123}, + "t", + new int[] {456, 666}, + "c", + null, + false); + } + + @Test + public void testSelinuxAuditLogsBuilder_withPath() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 path=\"/very/long/path\"" + + " tcontext=u:object_r:t:s0 tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + null, + "t", + null, + "c", + "/very/long", + false); + } + + @Test + public void testSelinuxAuditLogsBuilder_withPermissive() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 permissive=0" + + " tcontext=u:object_r:t:s0 tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + null, + "t", + null, + "c", + null, + false); + + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 tcontext=u:object_r:t:s0 tclass=c" + + " permissive=1"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + null, + "t", + null, + "c", + null, + true); + } + + private void assertAuditLog( + SelinuxAuditLog auditLog, + boolean granted, + String[] permissions, + String sType, + String tType, + String tClass) { + assertAuditLog( + auditLog, granted, permissions, sType, null, tType, null, tClass, null, false); + } + + private void assertAuditLog( + SelinuxAuditLog auditLog, + boolean granted, + String[] permissions, + String sType, + int[] sCategories, + String tType, + int[] tCategories, + String tClass, + String path, + boolean permissive) { + assertThat(auditLog).isNotNull(); + assertThat(auditLog.mGranted).isEqualTo(granted); + assertThat(auditLog.mPermissions).isEqualTo(permissions); + assertThat(auditLog.mSType).isEqualTo(sType); + assertThat(auditLog.mSCategories).isEqualTo(sCategories); + assertThat(auditLog.mTType).isEqualTo(tType); + assertThat(auditLog.mTCategories).isEqualTo(tCategories); + assertThat(auditLog.mTClass).isEqualTo(tClass); + assertThat(auditLog.mPath).isEqualTo(path); + assertThat(auditLog.mPermissive).isEqualTo(permissive); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java new file mode 100644 index 000000000000..9758ea596335 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java @@ -0,0 +1,644 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.server.selinux; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +import android.util.EventLog; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.os.Clock; +import com.android.internal.util.FrameworkStatsLog; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoSession; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.stream.Collectors; + +@RunWith(AndroidJUnit4.class) +public class SelinuxAuditLogsCollectorTest { + + // Fake tag to use for testing + private static final int ANSWER_TAG = 42; + + private final MockClock mClock = new MockClock(); + + private final SelinuxAuditLogsCollector mSelinuxAutidLogsCollector = + // Ignore rate limiting for tests + new SelinuxAuditLogsCollector( + new RateLimiter(mClock, /* window= */ Duration.ofMillis(0)), + new QuotaLimiter( + mClock, /* windowSize= */ Duration.ofHours(1), /* maxPermits= */ 5)); + + private MockitoSession mMockitoSession; + + @Before + public void setUp() { + // move the clock forward for the limiters. + mClock.currentTimeMillis += Duration.ofHours(1).toMillis(); + // Ignore what was written in the event logs by previous tests. + mSelinuxAutidLogsCollector.mLastWrite = Instant.now(); + + mMockitoSession = + mockitoSession().initMocks(this).mockStatic(FrameworkStatsLog.class).startMocking(); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + + @Test + public void testWriteSdkSandboxAuditLogs() { + writeTestLog("granted", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm1", "sdk_sandbox_audit", "ttype1", "tclass1"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + true, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm1"}, + "sdk_sandbox_audit", + null, + "ttype1", + null, + "tclass1", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_multiplePerms() { + writeTestLog("denied", "perm1 perm2", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm3 perm4", "sdk_sandbox_audit", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm1", "perm2"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm3", "perm4"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_withPaths() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/good/path"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/very/long/path"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/short_path"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "not_a_path"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + "/good/path", + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + "/very/long", + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + "/short_path", + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_withCategories() { + writeTestLog( + "denied", "perm", "sdk_sandbox_audit", new int[] {123}, "ttype", null, "tclass"); + writeTestLog( + "denied", + "perm", + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + null, + "tclass"); + writeTestLog( + "denied", "perm", "sdk_sandbox_audit", null, "ttype", new int[] {666}, "tclass"); + writeTestLog( + "denied", + "perm", + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + new int[] {666, 777}, + "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123}, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + new int[] {666}, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + new int[] {666, 777}, + "tclass", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_withPathAndCategories() { + writeTestLog( + "denied", + "perm", + "sdk_sandbox_audit", + new int[] {123}, + "ttype", + new int[] {666}, + "tclass", + "/a/path"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123}, + "ttype", + new int[] {666}, + "tclass", + "/a/path", + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_permissive() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", true); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", false); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false), + times(2)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + true)); + } + + @Test + public void testNotWriteAuditLogs_notSdkSandbox() { + writeTestLog("denied", "perm", "stype", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + never()); + } + + @Test + public void testWriteSdkSandboxAuditLogs_upToQuota() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + // These are not pushed. + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(5)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_resetQuota() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(5)); + + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + // move the clock forward to reset the quota limiter. + mClock.currentTimeMillis += Duration.ofHours(1).toMillis(); + done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(10)); + } + + @Test + public void testNotWriteAuditLogs_stopRequested() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + // These are not pushed. + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + + mSelinuxAutidLogsCollector.mStopRequested.set(true); + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isFalse(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + never()); + + mSelinuxAutidLogsCollector.mStopRequested.set(false); + done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(5)); + } + + @Test + public void testAuditLogs_resumeJobDoesNotExceedLimit() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + mSelinuxAutidLogsCollector.mStopRequested.set(true); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isFalse(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + never()); + } + + private static void writeTestLog( + String granted, String permissions, String sType, String tType, String tClass) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0 tclass=%s", + granted, permissions, sType, tType, tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + String tType, + String tClass, + String path) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } path=\"%s\" scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0" + + " tclass=%s", + granted, permissions, path, sType, tType, tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + int[] sCategories, + String tType, + int[] tCategories, + String tClass) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } scontext=u:r:%s:s0%s tcontext=u:object_r:%s:s0%s tclass=%s", + granted, + permissions, + sType, + toCategoriesString(sCategories), + tType, + toCategoriesString(tCategories), + tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + int[] sCategories, + String tType, + int[] tCategories, + String tClass, + String path) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } path=\"%s\" scontext=u:r:%s:s0%s" + + " tcontext=u:object_r:%s:s0%s tclass=%s", + granted, + permissions, + path, + sType, + toCategoriesString(sCategories), + tType, + toCategoriesString(tCategories), + tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + String tType, + String tClass, + boolean permissive) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0 tclass=%s" + + " permissive=%s", + granted, permissions, sType, tType, tClass, permissive ? "1" : "0")); + } + + private static String toCategoriesString(int[] categories) { + return (categories == null || categories.length == 0) + ? "" + : ":c" + + Arrays.stream(categories) + .mapToObj(String::valueOf) + .collect(Collectors.joining(",c")); + } + + private static final class MockClock extends Clock { + + public long currentTimeMillis = 0; + + @Override + public long currentTimeMillis() { + return currentTimeMillis; + } + } +} |