From charlesreid1

Requirements

  • Define a Detector structure (config, public functions) that provides an interface for the main loop to interact with results
  • Implement logic for filtering alters - consult the history log before flagging a new threat. (Ensures persistent rogue AP only triggers user notification once, instead of on every single scan.)
  • Detector should be stateful - maintain a history of what it has seen over time
  • Two-stage filtering - incoming analysis should be filtered first by score (is the suspicion above the alert threshold), and by history (is the BSSID already in history log)
  • Separate list of new detections (critical link to UI - UI polls this list after a scan, and generates alerts from any items that show up in this list)


detector.h

#ifndef DETECTOR_H
#define DETECTOR_H

#include "analyzer.h" // Depends on the AnalysisResult structure
#include <furi.h>

#define DETECTOR_MAX_HISTORY 100     // Max number of threats to remember
#define DETECTOR_MAX_NEW_ALERTS 10 // Max new alerts per scan cycle

/**
 * @brief User-configurable settings for the detector.
 */
typedef struct {
    int alert_threshold;                // Suspicion score (0-100) above which an alert is triggered.
    uint32_t history_timeout_seconds;   // How long an acknowledged alert stays in history (e.g., 24 hours).
} DetectorConfig;

/**
 * @brief An entry in the detection history log.
 * Used to prevent spamming alerts for the same device repeatedly.
 */
typedef struct {
    uint8_t bssid[6];           // The BSSID of the detected AP.
    uint32_t last_seen_ts;      // Timestamp of the last time it was seen.
    ThreatType threat_type;     // The original threat classification.
    bool acknowledged;          // True if the user has dismissed the alert for this AP.
} DetectionHistoryEntry;

/**
 * @brief Main detector state structure.
 */
typedef struct {
    DetectorConfig config;                  // Current configuration.
    
    DetectionHistoryEntry* history;         // Array of previously detected threats.
    uint16_t history_count;
    uint16_t history_capacity;

    AnalysisResult* new_detections;         // Array of threats detected in the CURRENT scan cycle.
    uint16_t new_detection_count;
    uint16_t new_detection_capacity;

    FuriMutex* mutex;                       // Mutex for thread-safe access.
} Detector;

/**
 * @brief Allocates and initializes a new Detector instance.
 * @return Pointer to the newly created Detector.
 */
Detector* detector_alloc();

/**
 * @brief Frees all resources associated with a Detector instance.
 * @param detector Pointer to the Detector instance to free.
 */
void detector_free(Detector* detector);

/**
 * @brief Updates the detector's configuration.
 * @param detector Pointer to the Detector instance.
 * @param config Pointer to the new configuration settings.
 */
void detector_set_config(Detector* detector, const DetectorConfig* config);

/**
 * @brief Processes a single analysis result to determine if it's a new, alert-worthy threat.
 * @param detector Pointer to the Detector instance.
 * @param result The result from the analyzer module.
 */
void detector_process_analysis(Detector* detector, const AnalysisResult* result);

/**
 * @brief Clears the list of new detections. Should be called at the start of each scan cycle.
 * @param detector Pointer to the Detector instance.
 */
void detector_clear_new_detections(Detector* detector);

/**
 * @brief Gets the list of newly detected threats from the last processing cycle.
 * @param detector Pointer to the Detector instance.
 * @param buffer A user-provided buffer to copy the results into.
 * @param buffer_size The number of elements the buffer can hold.
 * @return The number of new detections copied to the buffer.
 */
uint16_t detector_get_new_detections(Detector* detector, AnalysisResult* buffer, uint16_t buffer_size);

/**
 * @brief Marks a specific threat (by BSSID) in the history as acknowledged by the user.
 * @param detector Pointer to the Detector instance.
 * @param bssid The BSSID of the threat to acknowledge.
 */
void detector_acknowledge_threat(Detector* detector, const uint8_t bssid[6]);

#endif // DETECTOR_H

detector.c

#include "detector.h"
#include <furi_hal_rtc.h>
#include <string.h>

// --- Public Function Implementations ---

Detector* detector_alloc() {
    Detector* detector = malloc(sizeof(Detector));
    furi_check(detector != NULL);

    // Default configuration
    detector->config.alert_threshold = 70; // Alert on scores 70 and above
    detector->config.history_timeout_seconds = 86400; // 24 hours

    // Allocate history buffer
    detector->history_capacity = DETECTOR_MAX_HISTORY;
    detector->history = malloc(sizeof(DetectionHistoryEntry) * detector->history_capacity);
    furi_check(detector->history != NULL);
    detector->history_count = 0;

    // Allocate new detections buffer
    detector->new_detection_capacity = DETECTOR_MAX_NEW_ALERTS;
    detector->new_detections = malloc(sizeof(AnalysisResult) * detector->new_detection_capacity);
    furi_check(detector->new_detections != NULL);
    detector->new_detection_count = 0;

    detector->mutex = furi_mutex_alloc(FuriMutexTypeRecursive);

    return detector;
}

void detector_free(Detector* detector) {
    if(!detector) return;
    furi_mutex_free(detector->mutex);
    free(detector->history);
    free(detector->new_detections);
    free(detector);
}

void detector_set_config(Detector* detector, const DetectorConfig* config) {
    furi_check(detector && config);
    if(furi_mutex_acquire(detector->mutex, FuriWaitForever) != FuriStatusOk) return;
    
    memcpy(&detector->config, config, sizeof(DetectorConfig));
    
    furi_mutex_release(detector->mutex);
}

void detector_clear_new_detections(Detector* detector) {
    furi_check(detector);
    if(furi_mutex_acquire(detector->mutex, FuriWaitForever) != FuriStatusOk) return;

    detector->new_detection_count = 0;

    furi_mutex_release(detector->mutex);
}

void detector_process_analysis(Detector* detector, const AnalysisResult* result) {
    furi_check(detector && result);

    // --- Filter 1: Is the suspicion score high enough to be considered a threat? ---
    if(result->suspicion_score < detector->config.alert_threshold) {
        return; // Not suspicious enough, ignore it.
    }

    if(furi_mutex_acquire(detector->mutex, FuriWaitForever) != FuriStatusOk) return;

    // --- Filter 2: Have we already logged this BSSID in our history? ---
    bool found_in_history = false;
    for(uint16_t i = 0; i < detector->history_count; i++) {
        if(memcmp(detector->history[i].bssid, result->scanned_ap.bssid, 6) == 0) {
            // We've seen it. Just update its timestamp.
            detector->history[i].last_seen_ts = furi_hal_rtc_get_timestamp();
            found_in_history = true;
            break;
        }
    }

    // If it's a known threat, we don't need to generate a *new* alert.
    if(found_in_history) {
        furi_mutex_release(detector->mutex);
        return;
    }

    // --- It's a NEW threat. Add it to the history and the new detections list. ---

    // Add to history
    if(detector->history_count < detector->history_capacity) {
        DetectionHistoryEntry* new_entry = &detector->history[detector->history_count];
        memcpy(new_entry->bssid, result->scanned_ap.bssid, 6);
        new_entry->last_seen_ts = furi_hal_rtc_get_timestamp();
        new_entry->threat_type = result->threat_type;
        new_entry->acknowledged = false; // New threats are never acknowledged yet.
        detector->history_count++;
    }

    // Add to this cycle's list of new detections for the UI to display
    if(detector->new_detection_count < detector->new_detection_capacity) {
        memcpy(&detector->new_detections[detector->new_detection_count], result, sizeof(AnalysisResult));
        detector->new_detection_count++;
    }

    furi_mutex_release(detector->mutex);
}

uint16_t detector_get_new_detections(Detector* detector, AnalysisResult* buffer, uint16_t buffer_size) {
    furi_check(detector && buffer);
    if(furi_mutex_acquire(detector->mutex, FuriWaitForever) != FuriStatusOk) return 0;

    uint16_t count_to_copy = (detector->new_detection_count < buffer_size) 
        ? detector->new_detection_count 
        : buffer_size;
    
    if(count_to_copy > 0) {
        memcpy(buffer, detector->new_detections, count_to_copy * sizeof(AnalysisResult));
    }

    furi_mutex_release(detector->mutex);
    return count_to_copy;
}

void detector_acknowledge_threat(Detector* detector, const uint8_t bssid[6]) {
    furi_check(detector && bssid);
    if(furi_mutex_acquire(detector->mutex, FuriWaitForever) != FuriStatusOk) return;

    for(uint16_t i = 0; i < detector->history_count; i++) {
        if(memcmp(detector->history[i].bssid, bssid, 6) == 0) {
            detector->history[i].acknowledged = true;
            break;
        }
    }

    furi_mutex_release(detector->mutex);
}