Files
MosisService/SANDBOX_MILESTONE_3.md

8.6 KiB

Milestone 3: Audit Logging & Rate Limiting

Status: Complete ✓ Goal: Track security events and prevent API abuse.


Overview

This milestone adds security event logging and rate limiting to prevent abuse. The audit log tracks permission checks, sandbox violations, and resource usage. The rate limiter uses a token bucket algorithm to limit API call frequency.

Key Deliverables

  1. AuditLog class - Security event logging with ring buffer
  2. RateLimiter class - Token bucket rate limiting
  3. Thread safety - Concurrent access support

File Structure

src/main/cpp/sandbox/
├── lua_sandbox.h           # (existing)
├── lua_sandbox.cpp         # (existing)
├── permission_gate.h       # (existing)
├── permission_gate.cpp     # (existing)
├── audit_log.h             # NEW - Audit logging
├── audit_log.cpp           # NEW - Implementation
├── rate_limiter.h          # NEW - Rate limiting
└── rate_limiter.cpp        # NEW - Implementation

Implementation Details

1. AuditEvent Enum

// audit_log.h
enum class AuditEvent {
    // Lifecycle
    AppStart,
    AppStop,

    // Permissions
    PermissionCheck,
    PermissionGranted,
    PermissionDenied,

    // Network
    NetworkRequest,
    NetworkBlocked,

    // Storage
    FileAccess,
    FileBlocked,
    DatabaseAccess,

    // Hardware
    CameraAccess,
    MicrophoneAccess,
    LocationAccess,

    // Security
    SandboxViolation,
    ResourceLimitHit,
    RateLimitHit,

    // Other
    Custom
};

2. AuditLog Class

// audit_log.h
#pragma once

#include <string>
#include <vector>
#include <mutex>
#include <chrono>

namespace mosis {

enum class AuditEvent { /* ... */ };

struct AuditEntry {
    std::chrono::system_clock::time_point timestamp;
    AuditEvent event;
    std::string app_id;
    std::string details;
    bool success;
};

class AuditLog {
public:
    explicit AuditLog(size_t max_entries = 10000);

    // Log an event
    void Log(AuditEvent event, const std::string& app_id,
             const std::string& details = "", bool success = true);

    // Query entries
    std::vector<AuditEntry> GetEntries(size_t count = 100) const;
    std::vector<AuditEntry> GetEntriesForApp(const std::string& app_id,
                                              size_t count = 100) const;
    std::vector<AuditEntry> GetEntriesByEvent(AuditEvent event,
                                               size_t count = 100) const;

    // Statistics
    size_t GetTotalEntries() const;
    size_t CountEvents(AuditEvent event, const std::string& app_id = "") const;

    // Clear
    void Clear();

private:
    mutable std::mutex m_mutex;
    std::vector<AuditEntry> m_entries;
    size_t m_max_entries;
    size_t m_write_index = 0;
    size_t m_total_logged = 0;
};

// Global audit log (singleton pattern)
AuditLog& GetAuditLog();

} // namespace mosis

3. RateLimiter Class (Token Bucket)

// rate_limiter.h
#pragma once

#include <string>
#include <unordered_map>
#include <mutex>
#include <chrono>

namespace mosis {

struct RateLimitConfig {
    double tokens_per_second;    // Refill rate
    double max_tokens;           // Bucket capacity
};

class RateLimiter {
public:
    // Default limits for common operations
    RateLimiter();

    // Check if operation is allowed (consumes token if yes)
    bool Check(const std::string& app_id, const std::string& operation);

    // Check without consuming
    bool CanProceed(const std::string& app_id, const std::string& operation) const;

    // Configure limits for an operation
    void SetLimit(const std::string& operation, const RateLimitConfig& config);

    // Get current token count
    double GetTokens(const std::string& app_id, const std::string& operation) const;

    // Reset an app's tokens (e.g., on app restart)
    void ResetApp(const std::string& app_id);

private:
    struct Bucket {
        double tokens;
        std::chrono::steady_clock::time_point last_refill;
    };

    void Refill(Bucket& bucket, const RateLimitConfig& config) const;
    Bucket& GetBucket(const std::string& app_id, const std::string& operation);

    mutable std::mutex m_mutex;
    std::unordered_map<std::string, RateLimitConfig> m_configs;
    std::unordered_map<std::string, Bucket> m_buckets;  // Key: app_id:operation
};

// Global rate limiter
RateLimiter& GetRateLimiter();

} // namespace mosis

4. Default Rate Limits

// rate_limiter.cpp
RateLimiter::RateLimiter() {
    // Network
    SetLimit("network.request", {10.0, 100.0});   // 10/sec, burst 100
    SetLimit("network.websocket", {2.0, 10.0});   // 2/sec, burst 10

    // Storage
    SetLimit("storage.read", {100.0, 500.0});     // 100/sec, burst 500
    SetLimit("storage.write", {20.0, 100.0});     // 20/sec, burst 100
    SetLimit("database.query", {50.0, 200.0});    // 50/sec, burst 200

    // Hardware
    SetLimit("camera.capture", {30.0, 30.0});     // 30 fps max
    SetLimit("microphone.record", {1.0, 1.0});    // 1 session at a time
    SetLimit("location.request", {1.0, 5.0});     // 1/sec, burst 5

    // Timers
    SetLimit("timer.create", {10.0, 100.0});      // 10/sec, burst 100
}

Test Cases

Test 1: Audit Log Basic

bool Test_AuditLogBasic(std::string& error_msg) {
    mosis::AuditLog log(1000);

    log.Log(mosis::AuditEvent::AppStart, "test.app", "App started");
    log.Log(mosis::AuditEvent::PermissionCheck, "test.app", "camera", true);
    log.Log(mosis::AuditEvent::PermissionDenied, "test.app", "microphone", false);

    auto entries = log.GetEntries(10);
    EXPECT_TRUE(entries.size() == 3);

    auto app_entries = log.GetEntriesForApp("test.app", 10);
    EXPECT_TRUE(app_entries.size() == 3);

    return true;
}

Test 2: Audit Log Ring Buffer

bool Test_AuditLogRingBuffer(std::string& error_msg) {
    mosis::AuditLog log(100);  // Small buffer

    // Log more than capacity
    for (int i = 0; i < 200; i++) {
        log.Log(mosis::AuditEvent::Custom, "test.app", std::to_string(i));
    }

    // Should only have latest 100
    auto entries = log.GetEntries(200);
    EXPECT_TRUE(entries.size() == 100);

    // Total logged should be 200
    EXPECT_TRUE(log.GetTotalEntries() == 200);

    return true;
}

Test 3: Rate Limiter Basic

bool Test_RateLimiterBasic(std::string& error_msg) {
    mosis::RateLimiter limiter;

    // Should succeed initially (has tokens)
    EXPECT_TRUE(limiter.Check("test.app", "network.request"));

    return true;
}

Test 4: Rate Limiter Exhaustion

bool Test_RateLimiterExhaustion(std::string& error_msg) {
    mosis::RateLimiter limiter;
    limiter.SetLimit("test.op", {0.0, 5.0});  // 5 tokens, no refill

    // Use all tokens
    for (int i = 0; i < 5; i++) {
        EXPECT_TRUE(limiter.Check("test.app", "test.op"));
    }

    // Should be denied now
    EXPECT_FALSE(limiter.Check("test.app", "test.op"));

    return true;
}

Test 5: Rate Limiter Refill

bool Test_RateLimiterRefill(std::string& error_msg) {
    mosis::RateLimiter limiter;
    limiter.SetLimit("test.op", {1000.0, 1.0});  // 1000/sec, max 1

    // Use the token
    EXPECT_TRUE(limiter.Check("test.app", "test.op"));
    EXPECT_FALSE(limiter.Check("test.app", "test.op"));

    // Wait a bit for refill (1ms = 1 token at 1000/sec)
    std::this_thread::sleep_for(std::chrono::milliseconds(2));

    // Should have token again
    EXPECT_TRUE(limiter.Check("test.app", "test.op"));

    return true;
}

Test 6: App Isolation

bool Test_RateLimiterAppIsolation(std::string& error_msg) {
    mosis::RateLimiter limiter;
    limiter.SetLimit("test.op", {0.0, 1.0});  // 1 token, no refill

    // App 1 uses its token
    EXPECT_TRUE(limiter.Check("app1", "test.op"));
    EXPECT_FALSE(limiter.Check("app1", "test.op"));

    // App 2 should still have its token
    EXPECT_TRUE(limiter.Check("app2", "test.op"));

    return true;
}

Acceptance Criteria

All tests must pass:

  • Test_AuditLogBasic - Logging and retrieval work
  • Test_AuditLogRingBuffer - Ring buffer wraps correctly
  • Test_AuditLogThreadSafe - Thread safety verified
  • Test_RateLimiterBasic - Basic rate limiting works
  • Test_RateLimiterExhaustion - Tokens exhaust correctly
  • Test_RateLimiterRefill - Tokens refill over time
  • Test_RateLimiterAppIsolation - Apps have separate buckets
  • Test_RateLimiterReset - App reset clears buckets
  • Test_RateLimiterNoConfig - Unconfigured ops allowed

Next Steps

After Milestone 3 passes:

  1. Integrate AuditLog into LuaSandbox and PermissionGate
  2. Integrate RateLimiter into API implementations
  3. Milestone 4: Safe Path & Require