File System Events (далее FSEvents) - механизм, доступный в macOS начиная с версии 10.5, который позволяет подписываться на уведомления об изменениях в структуре папок, а также их содержимого. Он используется многими системными утилитами, например Time Machine. Стоит отметить, что мониторинг происходит на уровне директории: иными словами придет уведомление, что в директории изменился некий файл, но конкретной информации об изменениях в нём содержаться не будет. Для более детального мониторинга придётся спускаться на уровень ниже и использовать kqueues, либо писать собственный kext. Поэтому для написания собственного антивируса такой механизм не подойдет, а вот для своего аналога Dropbox, или чего-то подобного - вполне.
Механизм FSEvents состоит из трёх базовых частей:
- Код ядра транслирует события в пространство пользователя;
- Демон fseventsd обрабатывает эти события и рассылает уведомления;
- База данных, где хранятся записанные логи.
Изначально информация о событиях хранится в памяти. Когда происходит событие, ему назначается 64-битный идентификатор. Может получиться так что событие пришло с уже назначенным идентификатором, в этом случае, просто обновятся флаги. После того, как буфер памяти заполнится события записываются на диск.
Если выполнить в терминале ls -la /
, можно увидеть в корне раздела директорию .fseventsd
, куда упомянутый демон пишет логи. Посмотреть содержимое файлов так просто не получится, для этой цели можно воспользоваться скриптом FSEventsParser, он поможет экспортировать их в текстовом формате, либо как базу данных SQLite. Второй вариант обычно предпочтительнее, так как он позволит использовать SQL-запросы для фильтрации событий (которых обычно больше миллиона).
В целях безопасности, уведомления о событиях возвращаются только для тех папок, к которым есть доступ у пользователя, под которым запущена программа, подписанная на эти события. Существует возможность и полностью запретить логирование событий файловой системы на уровне раздела: для этого в корне нужно создать директорию .fseventsd
, куда положить пустой файл с названием no_log
.
Может показаться, что исторические данные из FSEvents - не самая полезная информация, однако косвенно они могут помочь, например при исследовании атакованной системы. Так как малварь обычно чистит за собой следы, то записи о создании подозрительных plist’ов, например в /Library/LaunchDaemons
, теоретически могут помочь идентифицировать поработавшего зловреда, что станет весомым артефактом в дальнейшем расследовании. Также по записям можно увидеть активность в домашней директории пользователя, файлы перемещенные в корзину, активность в интернете и многое другое. На iOS тоже можно получить данные из FSEvents, но для этого понадобится джейлбрейк.
Подписываемся на события программно
Если тебя больше интересует возможность подписаться на события и слушать их программно, то предлагаю реализовать для этого несложный класс, который будет подписываться на уведомления в заданной директории и возвращать информацию об изменениях (путь к файлу и флаги изменений) в колбек.
Подписываемся на поток событий файловой системы
Откроем Xcode и создадим новый проект, это будет Cocoa App (на вкладке macOS), язык выберем Swift. Создадим новый класс (File > New > File > macOS > Swift file) под названием FSEventsService и напишем следующий код:
class FSEventsService {
/// Колбек в который будут приходить события
var callback: ((FSEvent) -> Void)?
/// Директории, которые мы будем мониторить
let pathsToWatch: [String]
/// Ссылка на поток событий от файловой системы
private var stream: FSEventStreamRef!
init(pathsToWatch: [String], callback: @escaping (FSEvent) -> Void) {
self.pathsToWatch = pathsToWatch
self.callback = callback
}
}
Здесь мы просто объявили класс и создали несколько переменных. Теперь нам нужно запустить мониторинг событий, для этого определим следующий метод:
func start() {
var context = FSEventStreamContext(version: 0,
info: nil,
retain: nil,
release: nil,
copyDescription: nil)
context.info = Unmanaged.passUnretained(self).toOpaque()
let flags = UInt32(kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents)
stream = FSEventStreamCreate(kCFAllocatorDefault,
eventCallback,
&context,
pathsToWatch as CFArray,
FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
0,
flags)
FSEventStreamScheduleWithRunLoop(stream,
CFRunLoopGetMain(),
CFRunLoopMode.defaultMode.rawValue)
FSEventStreamStart(stream)
}
API FSEvents - это C API, мы можем его использовать из Swift, но нам потребуется учесть несколько особенностей. Разберем код метода по частям. Сначала мы создаем контекст и в него записываем указатель на наш экземпляр класса. Это нужно для того, чтобы иметь возможность потом к нему обратиться из C-функции, которую мы передаём в качестве коллбека, куда будут приходить события.
Далее определяем 2 флага: kFSEventStreamCreateFlagUseCFTypes
- говорит нашему потоку данных, чтобы он возвращал значения в типах Core Foundation, вместо чистых C-типов и kFSEventStreamCreateFlagFileEvents
- просит передавать события на уровне файлов, а не на уровне директории (иначе будут приходить только уведомления об изменениях в директории, но не будет информации, что за файл изменился).
Теперь осталось создать поток событий и передать в него необходимую информацию, а именно:
- Алокатор, используется для выделения памяти для потока. Используем значение по-умолчанию;
- Колбек, функция которая будет вызвана, когда произойдет событие, её реализуем ниже;
- Контекст, структура, которую мы создали ранее для того, чтобы передать указатель на экземпляр нашего класса;
- Пути к директориям, в которых нужно мониторить события;
- Идентификатор события, начиная с которого запрашивать события, выставляем с текущего момента;
- Задержка с которой нужно запрашивать у ядра события, здесь для простоты выставим 0, но вообще лучше поиграть с этим значением, для лучшей производительности;
- Флаги для изменения поведения потока, которые мы определили выше.
После этого остаётся только добавить поток событий в RunLoop (часть инфраструктуры отвечающая за обработку асинхронных событий, приходящих в поток) и запустить мониторинг.
Обрабатываем полученные события
Реализация колбэка будет выглядеть так:
private let eventCallback: FSEventStreamCallback = {
(stream: ConstFSEventStreamRef,
contextInfo: UnsafeMutableRawPointer?,
numEvents: Int,
eventPaths: UnsafeMutableRawPointer,
eventFlags: UnsafePointer<FSEventStreamEventFlags>,
eventIds: UnsafePointer<FSEventStreamEventId>) in
guard let contextInfo = contextInfo else { return }
let mySelf = Unmanaged<FSEventsService>.fromOpaque(contextInfo).takeUnretainedValue()
guard
let callback = mySelf.callback,
let paths = unsafeBitCast(eventPaths, to: NSArray.self) as? [String]
else {
return
}
for index in 0 ..< numEvents {
let event = FSEvent(identifier: eventIds[index],
path: paths[index],
flags: eventFlags[index])
callback(event)
}
}
Здесь мы получаем экземпляр нашего класса из контекста и записываем его в переменную mySelf. Из нее извлекаем колбек, который мы передали при инициализации нашего класса, пробегаем в цикле по пришедшим событиям, в нём создаем структуру, которая содержит информацию о событии (идентификатор события, путь и флаги) и передаем ее в этот коллбек.
Структура с информацией о событии выглядит следующим образом:
struct FSEvent {
let identifier: FSEventStreamEventId
let path: String
let flags: FSEventStreamEventFlags
}
Останавливаем получение событий
Здесь все достаточно просто: нужно вызвать несколько методов для остановки потока событий:
func stop() {
FSEventStreamStop(stream)
FSEventStreamInvalidate(stream)
FSEventStreamRelease(stream)
stream = nil
}
Вызов этого метода можно добавить, например, в deinit
:
deinit {
stop()
}
Использование класса
На этом написание простейшей обёртки закончено, мы теперь легко можем получить события от файловой системы, например:
let path = NSString(string: "~").expandingTildeInPath
fsEventsService = FSEventsService(pathsToWatch: [path]) { event in
print("\(event.identifier) - \(event.path) - \(event.flags)")
}
fsEventsService.start()
После запуска в логе приложения можно будет увидеть строки с измененными файлами. Перед запуском нужно не забыть отключить Sandbox (выбрать проект в инспекторе слева, перейти на вкладку Capabilities, отключить App Sandox), иначе у приложения будет доступ к файловой системе только внутри песочницы.
Фильтрация событий
В результате работы нашего класса, мы получили флаг события, который представляет собой битовую маску. Apple заранее определила определила для нас костанты с флагами, соотвествующими опредлённому типу события. Например, если мы захотим отфильтровать только события с созданием файла, это будет выглядеть следующим образом:
if eventFlags & UInt32(kFSEventStreamEventFlagItemCreated) != 0 {
// Обрабатываем событие создания
}
Подбирая комбинации флагов, можно гибко настроить получаемый поток событий под собственные нужды. Полный список флагов можно найти в документации.
Заключение
Сегодня мы рассмотрели реализацию собственной обертки над API FSEvents, если не считать взаимодействие с API на C, то использовать из Swift его довольно просто и не должно вызвать затруднений. На GitHub можно найти большое количество библиотек, схожих с тем, что мы реализовали сегодня. Пользоваться ими или нет, нужно решать исходя из конкретных задач: с одной стороны зачем писать лишний boilerplate, когда кто-то его уже написал за тебя, а с другой стороны в сложных решениях, всё равно приходится разбираться с внутренним устройством библиотек и докручивать их под специфичные нужды заказчика, так что может быть имеет смысл написать такой класс с нуля.