mirror of
https://github.com/google/santa.git
synced 2026-01-17 02:07:58 -05:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61558048c0 | ||
|
|
cf0e3fd3db | ||
|
|
15519c6de8 | ||
|
|
a415679980 | ||
|
|
27ae60e265 | ||
|
|
29a50f072c | ||
|
|
a97e82e316 | ||
|
|
532120ac02 | ||
|
|
ec934854fc | ||
|
|
ad0e2abdac | ||
|
|
dc11ea6534 | ||
|
|
3acf3c1d00 | ||
|
|
41bc3d2542 | ||
|
|
45a5d4e800 | ||
|
|
82bd981f31 | ||
|
|
6480d9c99b | ||
|
|
7e963080b3 | ||
|
|
e58cd7d125 | ||
|
|
db597e413b | ||
|
|
78f46896d5 | ||
|
|
cc0742dbfb | ||
|
|
9c2f76af72 | ||
|
|
a3ed5ccb40 | ||
|
|
b4149816c7 | ||
|
|
2313d6338d | ||
|
|
414fbff721 | ||
|
|
5a2e42e9b4 | ||
|
|
f8d1b2e880 | ||
|
|
5f4d2a92fc | ||
|
|
4ccffdca01 | ||
|
|
e60bbe1b55 | ||
|
|
eee2149439 |
31
.bazelrc
31
.bazelrc
@@ -9,17 +9,34 @@ build --cxxopt=-std=c++17
|
||||
build --copt=-DSANTA_OPEN_SOURCE=1
|
||||
build --cxxopt=-DSANTA_OPEN_SOURCE=1
|
||||
|
||||
build:asan --strip=never
|
||||
build:asan --copt="-Wno-macro-redefined"
|
||||
build:asan --copt="-D_FORTIFY_SOURCE=0"
|
||||
build:asan --copt="-O1"
|
||||
build:asan --copt="-fno-omit-frame-pointer"
|
||||
# Many config options for sanitizers pulled from
|
||||
# https://github.com/protocolbuffers/protobuf/blob/main/.bazelrc
|
||||
build:san-common --strip=never
|
||||
build:san-common --copt="-Wno-macro-redefined"
|
||||
build:san-common --copt="-D_FORTIFY_SOURCE=0"
|
||||
build:san-common --copt="-O1"
|
||||
build:san-common --copt="-fno-omit-frame-pointer"
|
||||
|
||||
build:asan --config=san-common
|
||||
build:asan --copt="-fsanitize=address"
|
||||
build:asan --copt="-DADDRESS_SANITIZER"
|
||||
build:asan --linkopt="-fsanitize=address"
|
||||
build:asan --action_env="ASAN_OPTIONS=log_path=/tmp/san_out"
|
||||
|
||||
build:fuzz --copt="-Wno-macro-redefined"
|
||||
build:fuzz --copt="-D_FORTIFY_SOURCE=0"
|
||||
build:tsan --config=san-common
|
||||
build:tsan --copt="-fsanitize=thread"
|
||||
build:tsan --copt="-DTHREAD_SANITIZER=1"
|
||||
build:tsan --linkopt="-fsanitize=thread"
|
||||
build:asan --action_env="TSAN_OPTIONS=log_path=/tmp/san_out"
|
||||
|
||||
build:ubsan --config=san-common
|
||||
build:ubsan --copt="-fsanitize=undefined"
|
||||
build:ubsan --copt="-DUNDEFINED_SANITIZER=1"
|
||||
build:ubsan --copt="-fno-sanitize=function" --copt="-fno-sanitize=vptr"
|
||||
build:ubsan --linkopt="-fsanitize=undefined"
|
||||
build:ubsan --action_env="UBSAN_OPTIONS=log_path=/tmp/san_out"
|
||||
|
||||
build:fuzz --config=san-common
|
||||
build:fuzz --@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing//fuzzing/engines:libfuzzer
|
||||
build:fuzz --@rules_fuzzing//fuzzing:cc_engine_instrumentation=libfuzzer
|
||||
build:fuzz --@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan
|
||||
|
||||
30
.github/workflows/sanitizers.yml
vendored
Normal file
30
.github/workflows/sanitizers.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: sanitizers
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 16 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
sanitizer: [asan, tsan, ubsan]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: ${{ matrix.sanitizer }}
|
||||
run: |
|
||||
CLANG_VERSION=$(clang --version | head -n 1 | cut -d' ' -f 4)
|
||||
DYLIB_PATH="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.${{ matrix.sanitizer }}_osx_dynamic.dylib"
|
||||
|
||||
bazel test --config=${{ matrix.sanitizer }} \
|
||||
--test_strategy=exclusive --test_output=errors \
|
||||
--test_env=DYLD_INSERT_LIBRARIES=${DYLIB_PATH} \
|
||||
--runs_per_test 5 -t- :unit_tests \
|
||||
--define=SANTA_BUILD_TYPE=adhoc
|
||||
- name: Upload logs
|
||||
uses: actions/upload-artifact@v1
|
||||
if: failure()
|
||||
with:
|
||||
name: logs
|
||||
path: /tmp/san_out*
|
||||
@@ -119,6 +119,10 @@ objc_library(
|
||||
name = "SNTDeviceEvent",
|
||||
srcs = ["SNTDeviceEvent.m"],
|
||||
hdrs = ["SNTDeviceEvent.h"],
|
||||
module_name = "santa_common_SNTDeviceEvent",
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
],
|
||||
@@ -133,6 +137,10 @@ objc_library(
|
||||
name = "SNTConfigurator",
|
||||
srcs = ["SNTConfigurator.m"],
|
||||
hdrs = ["SNTConfigurator.h"],
|
||||
module_name = "santa_common_SNTConfigurator",
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
":SNTRule",
|
||||
@@ -200,6 +208,9 @@ objc_library(
|
||||
name = "SNTRule",
|
||||
srcs = ["SNTRule.m"],
|
||||
hdrs = ["SNTRule.h"],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
":SNTSyncConstants",
|
||||
@@ -231,13 +242,19 @@ objc_library(
|
||||
name = "SNTSyncConstants",
|
||||
srcs = ["SNTSyncConstants.m"],
|
||||
hdrs = ["SNTSyncConstants.h"],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTSystemInfo",
|
||||
srcs = ["SNTSystemInfo.m"],
|
||||
hdrs = ["SNTSystemInfo.h"],
|
||||
sdk_frameworks = ["IOKit"],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
"IOKit",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
|
||||
@@ -194,6 +194,13 @@
|
||||
///
|
||||
@property(readonly, nonatomic) SNTEventLogType eventLogType;
|
||||
|
||||
///
|
||||
/// Returns the raw value of the EventLogType configuration key instead of being
|
||||
/// converted to the SNTEventLogType enum. If the key is not set, the default log
|
||||
/// type is returned.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *eventLogTypeRaw;
|
||||
|
||||
///
|
||||
/// If eventLogType is set to Filelog, eventLogPath will provide the path to save logs.
|
||||
/// Defaults to /var/db/santa/santa.log.
|
||||
|
||||
@@ -242,7 +242,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
|
||||
#pragma mark Singleton retriever
|
||||
|
||||
+ (instancetype)configurator {
|
||||
// The returned value is marked unsafe_unretained to avoid unnecessary retain/release handling.
|
||||
// The object returned is guaranteed to exist for the lifetime of the process so there's no need
|
||||
// to do this handling.
|
||||
+ (__unsafe_unretained instancetype)configurator {
|
||||
static SNTConfigurator *sharedConfigurator;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@@ -776,6 +779,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)eventLogTypeRaw {
|
||||
return self.configState[kEventLogType] ?: @"file";
|
||||
}
|
||||
|
||||
- (NSString *)eventLogPath {
|
||||
return self.configState[kEventLogPath] ?: @"/var/db/santa/santa.log";
|
||||
}
|
||||
|
||||
@@ -28,12 +28,16 @@ typedef struct SantaVnode {
|
||||
return fsid == rhs.fsid && fileid == rhs.fileid;
|
||||
}
|
||||
|
||||
static inline SantaVnode VnodeForFile(const es_file_t *es_file) {
|
||||
static inline SantaVnode VnodeForFile(const struct stat &sb) {
|
||||
return SantaVnode{
|
||||
.fsid = es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
.fsid = sb.st_dev,
|
||||
.fileid = sb.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline SantaVnode VnodeForFile(const es_file_t *es_file) {
|
||||
return VnodeForFile(es_file->stat);
|
||||
}
|
||||
#endif
|
||||
} SantaVnode;
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
//
|
||||
// !!! WARNING !!!
|
||||
// This proto is for demonstration purposes only and will be changing.
|
||||
// Do not rely on this format.
|
||||
//
|
||||
// Important: This schema is currently in BETA
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
load("@build_bazel_rules_apple//apple:macos.bzl", "macos_application")
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
|
||||
licenses(["notice"])
|
||||
@@ -11,6 +12,25 @@ exports_files([
|
||||
"Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-256.png",
|
||||
])
|
||||
|
||||
swift_library(
|
||||
name = "SNTAboutWindowView",
|
||||
srcs = ["SNTAboutWindowView.swift"],
|
||||
generates_header = 1,
|
||||
deps = ["//Source/common:SNTConfigurator"],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "SNTDeviceMessageWindowView",
|
||||
srcs = [
|
||||
"SNTDeviceMessageWindowView.swift",
|
||||
],
|
||||
generates_header = 1,
|
||||
deps = [
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SantaGUI_lib",
|
||||
srcs = [
|
||||
@@ -24,8 +44,6 @@ objc_library(
|
||||
"SNTBinaryMessageWindowController.m",
|
||||
"SNTDeviceMessageWindowController.h",
|
||||
"SNTDeviceMessageWindowController.m",
|
||||
"SNTMessageWindow.h",
|
||||
"SNTMessageWindow.m",
|
||||
"SNTMessageWindowController.h",
|
||||
"SNTMessageWindowController.m",
|
||||
"SNTNotificationManager.h",
|
||||
@@ -36,8 +54,6 @@ objc_library(
|
||||
"SNTNotificationManager.h",
|
||||
],
|
||||
data = [
|
||||
"Resources/AboutWindow.xib",
|
||||
"Resources/DeviceMessageWindow.xib",
|
||||
"Resources/MessageWindow.xib",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
@@ -47,6 +63,8 @@ objc_library(
|
||||
"UserNotifications",
|
||||
],
|
||||
deps = [
|
||||
":SNTAboutWindowView",
|
||||
":SNTDeviceMessageWindowView",
|
||||
"//Source/common:SNTBlockMessage_SantaGUI",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="SNTAboutWindowController">
|
||||
<connections>
|
||||
<outlet property="aboutTextField" destination="uh6-q0-RzL" id="oGn-hV-wwc"/>
|
||||
<outlet property="moreInfoButton" destination="SRu-Kf-vu5" id="Vj2-9Q-05d"/>
|
||||
<outlet property="window" destination="F0z-JX-Cv5" id="gIp-Ho-8D9"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Santa" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="F0z-JX-Cv5">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="200"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
|
||||
<view key="contentView" id="se5-gp-TjO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="200"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="BnL-ZS-kXw">
|
||||
<rect key="frame" x="199" y="140" width="83" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="40" id="UK2-2L-lPx"/>
|
||||
<constraint firstAttribute="width" constant="79" id="lDf-D7-qlY"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Santa" id="VVj-gU-bzy">
|
||||
<font key="font" size="34" name="HelveticaNeue-UltraLight"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uh6-q0-RzL">
|
||||
<rect key="frame" x="18" y="65" width="444" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" sendsActionOnEndEditing="YES" alignment="center" id="CcT-ul-1eA">
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="title">Santa is an application control system for macOS.
|
||||
|
||||
There are no user-configurable settings.</string>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SRu-Kf-vu5">
|
||||
<rect key="frame" x="129" y="21" width="113" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="99" id="JHv-2J-QSe"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="More Info..." bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6fe-ju-aET">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="openMoreInfoURL:" target="-2" id="dps-TN-rkS"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Udo-BY-n7e">
|
||||
<rect key="frame" x="239" y="21" width="113" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="99" id="2Xc-ax-2bV"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Dismiss" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="uSw-o1-lWW">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="orderOut:" target="-1" id="6oW-zI-zn5"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Udo-BY-n7e" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" priority="900" constant="191" id="1T4-DB-Dz8"/>
|
||||
<constraint firstItem="SRu-Kf-vu5" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="136" id="Ake-nU-qhW"/>
|
||||
<constraint firstItem="BnL-ZS-kXw" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="20" symbolic="YES" id="Fj1-SG-mzF"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Udo-BY-n7e" secondAttribute="bottom" constant="28" id="bpF-hC-haN"/>
|
||||
<constraint firstAttribute="bottom" secondItem="SRu-Kf-vu5" secondAttribute="bottom" constant="28" id="fCB-02-SEt"/>
|
||||
<constraint firstItem="BnL-ZS-kXw" firstAttribute="centerX" secondItem="se5-gp-TjO" secondAttribute="centerX" id="kez-S0-6Gg"/>
|
||||
<constraint firstItem="Udo-BY-n7e" firstAttribute="leading" secondItem="SRu-Kf-vu5" secondAttribute="trailing" constant="11" id="sYO-yY-w9w"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-2" id="0bl-1N-AYu"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="323" y="317"/>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -1,193 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="SNTDeviceMessageWindowController">
|
||||
<connections>
|
||||
<outlet property="window" destination="9Bq-yh-54f" id="Uhs-WF-TV9"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Santa Blocked Execution" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" visibleAtLaunch="NO" animationBehavior="none" id="9Bq-yh-54f" customClass="SNTMessageWindow">
|
||||
<windowStyleMask key="styleMask" utility="YES"/>
|
||||
<rect key="contentRect" x="167" y="107" width="515" height="318"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
|
||||
<view key="contentView" id="Iwq-Lx-rLv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="515" height="318"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<userGuides>
|
||||
<userLayoutGuide location="344" affinity="minX"/>
|
||||
</userGuides>
|
||||
<subviews>
|
||||
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kiB-jZ-69S">
|
||||
<rect key="frame" x="16" y="290" width="37" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="push" title="Hidden Button" alternateTitle="This button exists so neither of the other two buttons is pre-selected when the dialog opens." bezelStyle="rounded" alignment="center" borderStyle="border" focusRingType="none" transparent="YES" imageScaling="proportionallyDown" inset="2" id="XGa-Sl-F4t">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</buttonCell>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cD5-Su-lXR" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="31" y="210" width="454" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="450" id="XgJ-EV-tBa"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" refusesFirstResponder="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="center" title="A message to the user goes here..." allowsEditingTextAttributes="YES" id="5tH-bG-UJA">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.attributedCustomMessage" id="376-sj-4Q1"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="d9e-Wv-Y5H" userLabel="Label: Path">
|
||||
<rect key="frame" x="8" y="139" width="142" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="138" id="Kqd-nX-7df"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Device Name" id="KgY-X1-ESG">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YNz-ka-cBi" userLabel="Label: Path">
|
||||
<rect key="frame" x="8" y="115" width="142" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="138" id="3wU-P0-gAC"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Device BSD Path" id="adC-be-Beh">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField toolTip="Device Name" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" ambiguous="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pc8-G9-4pJ" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="187" y="139" width="315" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="311" id="xVR-j3-dLw"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Device Name" id="E7T-9h-ofr">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.mntonname" id="bOu-gv-1Vh"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField toolTip="Device BSD Path" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" ambiguous="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bDE-Tl-UHg" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="187" y="115" width="315" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="311" id="p1W-f9-KBX"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Device BSD Path" id="H1b-Ui-CYo">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.mntfromname" id="Sry-KY-HDb"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="t8c-Fx-e5h">
|
||||
<rect key="frame" x="217" y="248" width="82" height="40"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" refusesFirstResponder="YES" sendsActionOnEndEditing="YES" title="Santa" id="7YA-iB-Zma">
|
||||
<font key="font" metaFont="systemUltraLight" size="34"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="i8e-8n-tZS" userLabel="Label: Path">
|
||||
<rect key="frame" x="8" y="91" width="142" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="138" id="a3r-ng-zxJ"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Remount Mode" id="4D0-8L-ihK">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField toolTip="Device BSD Path" verticalHuggingPriority="750" ambiguous="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kmz-be-oWf" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="187" y="91" width="315" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="311" id="cbM-i5-bJ9"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Remount Mode" id="qI0-Na-kxN">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.readableRemountArgs" id="bOu-gv-1Vl"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="BbV-3h-mmL" userLabel="Dismiss Button">
|
||||
<rect key="frame" x="202" y="18" width="112" height="25"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="110" id="6Uh-Bd-N64"/>
|
||||
<constraint firstAttribute="height" constant="22" id="GH6-nw-6rD"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="roundTextured" title="Ok" bezelStyle="texturedRounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="XR6-Xa-gP4">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES"/>
|
||||
</buttonCell>
|
||||
<accessibility description="Dismiss Dialog"/>
|
||||
<connections>
|
||||
<action selector="closeWindow:" target="-2" id="qQq-gh-8lw"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="BbV-3h-mmL" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" priority="500" constant="193" id="2uo-Cm-Tfp"/>
|
||||
<constraint firstItem="kmz-be-oWf" firstAttribute="height" secondItem="i8e-8n-tZS" secondAttribute="height" id="6Ow-Sl-6Wc"/>
|
||||
<constraint firstItem="d9e-Wv-Y5H" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="FPe-Rd-G4n"/>
|
||||
<constraint firstItem="t8c-Fx-e5h" firstAttribute="top" secondItem="Iwq-Lx-rLv" secondAttribute="top" constant="30" id="FuB-GX-0jg"/>
|
||||
<constraint firstItem="YNz-ka-cBi" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="KmX-kX-VCN"/>
|
||||
<constraint firstItem="pc8-G9-4pJ" firstAttribute="top" secondItem="cD5-Su-lXR" secondAttribute="bottom" priority="950" constant="55" id="Nsl-zf-poH"/>
|
||||
<constraint firstItem="YNz-ka-cBi" firstAttribute="centerY" secondItem="bDE-Tl-UHg" secondAttribute="centerY" id="ObQ-RA-S5V"/>
|
||||
<constraint firstItem="d9e-Wv-Y5H" firstAttribute="centerY" secondItem="pc8-G9-4pJ" secondAttribute="centerY" id="SLv-F7-w5k"/>
|
||||
<constraint firstAttribute="centerX" secondItem="cD5-Su-lXR" secondAttribute="centerX" id="V0a-Py-iEc"/>
|
||||
<constraint firstItem="cD5-Su-lXR" firstAttribute="top" secondItem="t8c-Fx-e5h" secondAttribute="bottom" constant="22" id="dYg-zP-wh2"/>
|
||||
<constraint firstAttribute="centerX" secondItem="t8c-Fx-e5h" secondAttribute="centerX" id="h3d-Kc-q88"/>
|
||||
<constraint firstItem="kmz-be-oWf" firstAttribute="centerY" secondItem="i8e-8n-tZS" secondAttribute="centerY" id="ilC-5x-LNe"/>
|
||||
<constraint firstItem="bDE-Tl-UHg" firstAttribute="top" secondItem="pc8-G9-4pJ" secondAttribute="bottom" constant="8" id="pis-of-f93"/>
|
||||
<constraint firstItem="i8e-8n-tZS" firstAttribute="centerY" secondItem="YNz-ka-cBi" secondAttribute="centerY" constant="24" id="uZs-XT-PuZ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="BbV-3h-mmL" secondAttribute="bottom" constant="35" id="ukF-FH-DE8"/>
|
||||
<constraint firstItem="YNz-ka-cBi" firstAttribute="firstBaseline" secondItem="d9e-Wv-Y5H" secondAttribute="baseline" constant="24" id="xZu-AY-wX5"/>
|
||||
<constraint firstItem="i8e-8n-tZS" firstAttribute="top" secondItem="YNz-ka-cBi" secondAttribute="bottom" constant="8" symbolic="YES" id="ylw-3s-9JW"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="initialFirstResponder" destination="kiB-jZ-69S" id="I96-dS-lq5"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="261.5" y="246"/>
|
||||
</window>
|
||||
<userDefaultsController representsSharedInstance="YES" id="iXx-cu-WYe"/>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -14,11 +14,5 @@
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface SNTAboutWindowController : NSWindowController
|
||||
|
||||
@property IBOutlet NSTextField *aboutTextField;
|
||||
@property IBOutlet NSButton *moreInfoButton;
|
||||
|
||||
- (IBAction)openMoreInfoURL:(id)sender;
|
||||
|
||||
@interface SNTAboutWindowController : NSWindowController <NSWindowDelegate>
|
||||
@end
|
||||
|
||||
@@ -13,30 +13,35 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/gui/SNTAboutWindowController.h"
|
||||
#import "Source/gui/SNTAboutWindowView-Swift.h"
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
|
||||
@implementation SNTAboutWindowController
|
||||
|
||||
- (instancetype)init {
|
||||
return [super initWithWindowNibName:@"AboutWindow"];
|
||||
- (void)showWindow:(id)sender {
|
||||
[super showWindow:sender];
|
||||
|
||||
if (self.window) [self.window orderOut:sender];
|
||||
|
||||
self.window =
|
||||
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
|
||||
styleMask:NSWindowStyleMaskClosable | NSWindowStyleMaskTitled
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
self.window.contentViewController = [SNTAboutWindowViewFactory createWithWindow:self.window];
|
||||
self.window.title = @"Santa";
|
||||
self.window.delegate = self;
|
||||
[self.window makeKeyAndOrderFront:nil];
|
||||
[self.window center];
|
||||
|
||||
// Add app to Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
|
||||
}
|
||||
|
||||
- (void)loadWindow {
|
||||
[super loadWindow];
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
NSString *aboutText = [config aboutText];
|
||||
if (aboutText) {
|
||||
[self.aboutTextField setStringValue:aboutText];
|
||||
}
|
||||
if (![config moreInfoURL]) {
|
||||
[self.moreInfoButton removeFromSuperview];
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)openMoreInfoURL:(id)sender {
|
||||
[[NSWorkspace sharedWorkspace] openURL:[[SNTConfigurator configurator] moreInfoURL]];
|
||||
[self close];
|
||||
- (void)windowWillClose:(NSNotification *)notification {
|
||||
// Remove app from Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
63
Source/gui/SNTAboutWindowView.swift
Normal file
63
Source/gui/SNTAboutWindowView.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
import SwiftUI
|
||||
|
||||
import santa_common_SNTConfigurator
|
||||
|
||||
@objc public class SNTAboutWindowViewFactory : NSObject {
|
||||
@objc public static func createWith(window: NSWindow) -> NSViewController {
|
||||
return NSHostingController(rootView:SNTAboutWindowView(w:window).frame(width:400, height:200))
|
||||
}
|
||||
}
|
||||
|
||||
struct SNTAboutWindowView: View {
|
||||
let w: NSWindow?
|
||||
let c = SNTConfigurator()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing:20.0) {
|
||||
Text("Santa").font(Font.custom("HelveticaNeue-UltraLight", size: 34.0))
|
||||
|
||||
if let t = c.aboutText {
|
||||
Text(t).multilineTextAlignment(.center)
|
||||
} else {
|
||||
Text("""
|
||||
Santa is an application control system for macOS.
|
||||
|
||||
There are no user-configurable settings.
|
||||
""").multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
HStack {
|
||||
if c.moreInfoURL?.absoluteString.isEmpty == false {
|
||||
Button(action: moreInfoButton) {
|
||||
Text("More Info...").frame(width: 90.0)
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: dismissButton) {
|
||||
Text("Dismiss").frame(width: 90.0)
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
|
||||
}.padding(10.0)
|
||||
}
|
||||
}
|
||||
|
||||
func dismissButton() {
|
||||
w?.close()
|
||||
}
|
||||
|
||||
func moreInfoButton() {
|
||||
if let u = c.moreInfoURL {
|
||||
NSWorkspace.shared.open(u)
|
||||
}
|
||||
w?.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Enable previews in Xcode.
|
||||
struct SNTAboutWindow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SNTAboutWindowView(w: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@
|
||||
}
|
||||
|
||||
- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag {
|
||||
self.aboutWindowController = [[SNTAboutWindowController alloc] init];
|
||||
if (!self.aboutWindowController) {
|
||||
self.aboutWindowController = [[SNTAboutWindowController alloc] init];
|
||||
}
|
||||
[self.aboutWindowController showWindow:self];
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/gui/SNTMessageWindow.h"
|
||||
|
||||
@interface SNTBinaryMessageWindowController ()
|
||||
/// The custom message to display for this event
|
||||
|
||||
@@ -23,10 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
///
|
||||
/// Controller for a single message window.
|
||||
///
|
||||
@interface SNTDeviceMessageWindowController : SNTMessageWindowController
|
||||
|
||||
@property(weak) IBOutlet NSTextField *remountArgsLabel;
|
||||
@property(weak) IBOutlet NSTextField *remountArgsTitle;
|
||||
@interface SNTDeviceMessageWindowController : SNTMessageWindowController <NSWindowDelegate>
|
||||
|
||||
// The device event this window is for.
|
||||
@property(readonly) SNTDeviceEvent *event;
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/gui/SNTDeviceMessageWindowController.h"
|
||||
#import "Source/gui/SNTDeviceMessageWindowView-Swift.h"
|
||||
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/gui/SNTMessageWindow.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@implementation SNTDeviceMessageWindowController
|
||||
|
||||
- (instancetype)initWithEvent:(SNTDeviceEvent *)event message:(nullable NSString *)message {
|
||||
self = [super initWithWindowNibName:@"DeviceMessageWindow"];
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_event = event;
|
||||
_customMessage = message;
|
||||
@@ -36,12 +36,30 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)loadWindow {
|
||||
[super loadWindow];
|
||||
if (!self.event.remountArgs || [self.event.remountArgs count] <= 0) {
|
||||
[self.remountArgsLabel removeFromSuperview];
|
||||
[self.remountArgsTitle removeFromSuperview];
|
||||
}
|
||||
- (void)showWindow:(id)sender {
|
||||
if (self.window) [self.window orderOut:sender];
|
||||
|
||||
self.window =
|
||||
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
|
||||
styleMask:NSWindowStyleMaskClosable | NSWindowStyleMaskTitled
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
self.window.contentViewController =
|
||||
[SNTDeviceMessageWindowViewFactory createWithWindow:self.window
|
||||
event:self.event
|
||||
customMsg:self.attributedCustomMessage];
|
||||
self.window.delegate = self;
|
||||
|
||||
// Add app to Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
|
||||
|
||||
[super showWindow:sender];
|
||||
}
|
||||
|
||||
- (void)windowWillClose:(NSNotification *)notification {
|
||||
// Remove app from Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
|
||||
[super windowWillClose:notification];
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedCustomMessage {
|
||||
|
||||
76
Source/gui/SNTDeviceMessageWindowView.swift
Normal file
76
Source/gui/SNTDeviceMessageWindowView.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import SwiftUI
|
||||
|
||||
import santa_common_SNTConfigurator
|
||||
import santa_common_SNTDeviceEvent
|
||||
|
||||
@objc public class SNTDeviceMessageWindowViewFactory : NSObject {
|
||||
@objc public static func createWith(window: NSWindow, event: SNTDeviceEvent, customMsg: NSAttributedString?) -> NSViewController {
|
||||
return NSHostingController(rootView:SNTDeviceMessageWindowView(window:window, event:event, customMsg:customMsg).frame(width:450, height:300))
|
||||
}
|
||||
}
|
||||
|
||||
struct SNTDeviceMessageWindowView: View {
|
||||
let window: NSWindow?
|
||||
let event: SNTDeviceEvent?
|
||||
let customMsg: NSAttributedString?
|
||||
|
||||
let c = SNTConfigurator()
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing:20.0) {
|
||||
Text("Santa").font(Font.custom("HelveticaNeue-UltraLight", size: 34.0))
|
||||
|
||||
if let t = customMsg {
|
||||
if #available(macOS 12.0, *) {
|
||||
let a = AttributedString(t)
|
||||
Text(a).multilineTextAlignment(.center).padding(15.0)
|
||||
} else {
|
||||
Text(t.description).multilineTextAlignment(.center).padding(15.0)
|
||||
}
|
||||
} else {
|
||||
Text("Mounting devices is blocked")
|
||||
}
|
||||
|
||||
HStack(spacing:5.0) {
|
||||
VStack(alignment: .trailing, spacing: 8.0) {
|
||||
Text("Device Name").bold()
|
||||
Text("Device BSD Path").bold()
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
Text("Remount Mode").bold()
|
||||
}
|
||||
}
|
||||
Spacer().frame(width: 10.0)
|
||||
VStack(alignment: .leading, spacing: 8.0) {
|
||||
Text(event!.mntonname)
|
||||
Text(event!.mntfromname)
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
Text(event!.readableRemountArgs())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Button(action: dismissButton) {
|
||||
Text("OK").frame(width: 90.0)
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
|
||||
}.padding(10.0)
|
||||
}
|
||||
}
|
||||
|
||||
func dismissButton() {
|
||||
window?.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Enable previews in Xcode.
|
||||
struct SNTDeviceMessageWindowView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SNTDeviceMessageWindowView(window: nil, event: nil, customMsg: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- (void)windowDidCloseSilenceHash:(NSString *)hash;
|
||||
@end
|
||||
|
||||
@interface SNTMessageWindowController : NSWindowController
|
||||
@interface SNTMessageWindowController : NSWindowController <NSWindowDelegate>
|
||||
|
||||
- (IBAction)showWindow:(id)sender;
|
||||
- (IBAction)closeWindow:(id)sender;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
#import "Source/gui/SNTMessageWindowController.h"
|
||||
|
||||
#import "Source/gui/SNTMessageWindow.h"
|
||||
|
||||
@implementation SNTMessageWindowController
|
||||
|
||||
- (IBAction)showWindow:(id)sender {
|
||||
[(SNTMessageWindow *)self.window fadeIn:sender];
|
||||
[self.window setLevel:NSPopUpMenuWindowLevel];
|
||||
[self.window setMovableByWindowBackground:YES];
|
||||
[self.window makeKeyAndOrderFront:sender];
|
||||
[self.window center];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
}
|
||||
- (IBAction)closeWindow:(id)sender {
|
||||
[(SNTMessageWindow *)self.window fadeOut:sender];
|
||||
[self windowWillClose:sender];
|
||||
[self.window close];
|
||||
}
|
||||
|
||||
- (void)windowWillClose:(NSNotification *)notification {
|
||||
@@ -21,12 +24,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)loadWindow {
|
||||
[super loadWindow];
|
||||
[self.window setLevel:NSPopUpMenuWindowLevel];
|
||||
[self.window setMovableByWindowBackground:YES];
|
||||
}
|
||||
|
||||
- (NSString *)messageHash {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
|
||||
@@ -40,27 +40,27 @@ REGISTER_COMMAND_NAME(@"checkcache")
|
||||
}
|
||||
|
||||
+ (NSString *)shortHelpText {
|
||||
return @"Prints the status of a file in the kernel cache.";
|
||||
return @"Prints the status of a file in the cache.";
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
return (@"Checks the in-kernel cache for desired file.\n"
|
||||
return (@"Checks the cache for desired file.\n"
|
||||
@"Returns 0 if successful, 1 otherwise");
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
SantaVnode vnodeID = [self vnodeIDForFile:arguments.firstObject];
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
[[self.daemonConn synchronousRemoteObjectProxy]
|
||||
checkCacheForVnodeID:vnodeID
|
||||
withReply:^(SNTAction action) {
|
||||
if (action == SNTActionRespondAllow) {
|
||||
LOGI(@"File exists in [allowlist] kernel cache");
|
||||
LOGI(@"File exists in [allowlist] cache");
|
||||
exit(0);
|
||||
} else if (action == SNTActionRespondDeny) {
|
||||
LOGI(@"File exists in [blocklist] kernel cache");
|
||||
LOGI(@"File exists in [blocklist] cache");
|
||||
exit(0);
|
||||
} else if (action == SNTActionRespondAllowCompiler) {
|
||||
LOGI(@"File exists in [allowlist compiler] kernel cache");
|
||||
LOGI(@"File exists in [allowlist compiler] cache");
|
||||
exit(0);
|
||||
} else if (action == SNTActionUnset) {
|
||||
LOGE(@"File does not exist in cache");
|
||||
|
||||
@@ -166,19 +166,10 @@ REGISTER_COMMAND_NAME(@"metrics")
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
__block NSDictionary *metrics;
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
|
||||
[[self.daemonConn remoteObjectProxy] metrics:^(NSDictionary *exportedMetrics) {
|
||||
[[self.daemonConn synchronousRemoteObjectProxy] metrics:^(NSDictionary *exportedMetrics) {
|
||||
metrics = exportedMetrics;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Wait a maximum of 5s for metrics collected from daemon to arrive.
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
|
||||
fprintf(stderr, "Failed to retrieve metrics from daemon\n\n");
|
||||
}
|
||||
|
||||
metrics = [self filterMetrics:metrics withArguments:arguments];
|
||||
|
||||
[self prettyPrintMetrics:metrics asJSON:[arguments containsObject:@"--json"]];
|
||||
|
||||
@@ -210,87 +210,65 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
|
||||
- (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)daemonConn {
|
||||
id<SNTDaemonControlXPC> rop = [daemonConn synchronousRemoteObjectProxy];
|
||||
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil;
|
||||
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil;
|
||||
NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil;
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
__block NSMutableString *output;
|
||||
[[daemonConn remoteObjectProxy] decisionForFilePath:nil
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
reply:^(SNTEventState s) {
|
||||
output = (SNTEventStateAllow & s)
|
||||
? @"Allowed".mutableCopy
|
||||
: @"Blocked".mutableCopy;
|
||||
switch (s) {
|
||||
case SNTEventStateAllowUnknown:
|
||||
case SNTEventStateBlockUnknown:
|
||||
[output appendString:@" (Unknown)"];
|
||||
break;
|
||||
case SNTEventStateAllowBinary:
|
||||
case SNTEventStateBlockBinary:
|
||||
[output appendString:@" (Binary)"];
|
||||
break;
|
||||
case SNTEventStateAllowCertificate:
|
||||
case SNTEventStateBlockCertificate:
|
||||
[output appendString:@" (Certificate)"];
|
||||
break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope:
|
||||
[output appendString:@" (Scope)"];
|
||||
break;
|
||||
case SNTEventStateAllowCompiler:
|
||||
[output appendString:@" (Compiler)"];
|
||||
break;
|
||||
case SNTEventStateAllowTransitive:
|
||||
[output appendString:@" (Transitive)"];
|
||||
break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID:
|
||||
[output appendString:@" (TeamID)"];
|
||||
break;
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
if ((SNTEventStateAllow & s)) {
|
||||
[output insertString:@"\033[32m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else if ((SNTEventStateBlock & s)) {
|
||||
[output insertString:@"\033[31m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else {
|
||||
[output insertString:@"\033[33m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
}
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
printf("Cannot communicate with daemon");
|
||||
exit(1);
|
||||
}
|
||||
[rop decisionForFilePath:nil
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
reply:^(SNTEventState s) {
|
||||
output =
|
||||
(SNTEventStateAllow & s) ? @"Allowed".mutableCopy : @"Blocked".mutableCopy;
|
||||
switch (s) {
|
||||
case SNTEventStateAllowUnknown:
|
||||
case SNTEventStateBlockUnknown: [output appendString:@" (Unknown)"]; break;
|
||||
case SNTEventStateAllowBinary:
|
||||
case SNTEventStateBlockBinary: [output appendString:@" (Binary)"]; break;
|
||||
case SNTEventStateAllowCertificate:
|
||||
case SNTEventStateBlockCertificate:
|
||||
[output appendString:@" (Certificate)"];
|
||||
break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler:
|
||||
[output appendString:@" (Compiler)"];
|
||||
break;
|
||||
case SNTEventStateAllowTransitive:
|
||||
[output appendString:@" (Transitive)"];
|
||||
break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
if ((SNTEventStateAllow & s)) {
|
||||
[output insertString:@"\033[32m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else if ((SNTEventStateBlock & s)) {
|
||||
[output insertString:@"\033[31m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else {
|
||||
[output insertString:@"\033[33m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
[[daemonConn remoteObjectProxy]
|
||||
databaseRuleForBinarySHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
reply:^(SNTRule *r) {
|
||||
if (r.state == SNTRuleStateAllowTransitive) {
|
||||
NSDate *date =
|
||||
[NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp];
|
||||
[output
|
||||
appendString:[NSString stringWithFormat:@"\nlast access date: %@",
|
||||
[date description]]];
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
printf("Cannot communicate with daemon");
|
||||
exit(1);
|
||||
}
|
||||
[rop databaseRuleForBinarySHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
reply:^(SNTRule *r) {
|
||||
if (r.state == SNTRuleStateAllowTransitive) {
|
||||
NSDate *date =
|
||||
[NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp];
|
||||
[output appendString:[NSString
|
||||
stringWithFormat:@"\nlast access date: %@",
|
||||
[date description]]];
|
||||
}
|
||||
}];
|
||||
|
||||
printf("%s\n", output.UTF8String);
|
||||
exit(0);
|
||||
|
||||
@@ -46,116 +46,94 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
|
||||
// Daemon status
|
||||
__block NSString *clientMode;
|
||||
__block uint64_t cpuEvents, ramEvents;
|
||||
__block double cpuPeak, ramPeak;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] clientMode:^(SNTClientMode cm) {
|
||||
[rop clientMode:^(SNTClientMode cm) {
|
||||
switch (cm) {
|
||||
case SNTClientModeMonitor: clientMode = @"Monitor"; break;
|
||||
case SNTClientModeLockdown: clientMode = @"Lockdown"; break;
|
||||
default: clientMode = [NSString stringWithFormat:@"Unknown (%ld)", cm]; break;
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] watchdogInfo:^(uint64_t wd_cpuEvents, uint64_t wd_ramEvents,
|
||||
double wd_cpuPeak, double wd_ramPeak) {
|
||||
|
||||
[rop watchdogInfo:^(uint64_t wd_cpuEvents, uint64_t wd_ramEvents, double wd_cpuPeak,
|
||||
double wd_ramPeak) {
|
||||
cpuEvents = wd_cpuEvents;
|
||||
cpuPeak = wd_cpuPeak;
|
||||
ramEvents = wd_ramEvents;
|
||||
ramPeak = wd_ramPeak;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
BOOL fileLogging = ([[SNTConfigurator configurator] fileChangesRegex] != nil);
|
||||
NSString *eventLogType = [[[SNTConfigurator configurator] eventLogTypeRaw] lowercaseString];
|
||||
|
||||
SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
// Cache status
|
||||
__block uint64_t rootCacheCount = -1, nonRootCacheCount = -1;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) {
|
||||
[rop cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) {
|
||||
rootCacheCount = rootCache;
|
||||
nonRootCacheCount = nonRootCache;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Database counts
|
||||
__block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1, teamIDRuleCount = -1;
|
||||
__block int64_t compilerRuleCount = -1, transitiveRuleCount = -1;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler, int64_t transitive,
|
||||
int64_t teamID) {
|
||||
binaryRuleCount = binary;
|
||||
certRuleCount = certificate;
|
||||
teamIDRuleCount = teamID;
|
||||
compilerRuleCount = compiler;
|
||||
transitiveRuleCount = transitive;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] databaseEventCount:^(int64_t count) {
|
||||
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID) {
|
||||
binaryRuleCount = binary;
|
||||
certRuleCount = certificate;
|
||||
teamIDRuleCount = teamID;
|
||||
compilerRuleCount = compiler;
|
||||
transitiveRuleCount = transitive;
|
||||
}];
|
||||
[rop databaseEventCount:^(int64_t count) {
|
||||
eventCount = count;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Static rule count
|
||||
__block int64_t staticRuleCount = -1;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] staticRuleCount:^(int64_t count) {
|
||||
[rop staticRuleCount:^(int64_t count) {
|
||||
staticRuleCount = count;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Sync status
|
||||
__block NSDate *fullSyncLastSuccess;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] fullSyncLastSuccess:^(NSDate *date) {
|
||||
[rop fullSyncLastSuccess:^(NSDate *date) {
|
||||
fullSyncLastSuccess = date;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
__block NSDate *ruleSyncLastSuccess;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] ruleSyncLastSuccess:^(NSDate *date) {
|
||||
[rop ruleSyncLastSuccess:^(NSDate *date) {
|
||||
ruleSyncLastSuccess = date;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
__block BOOL syncCleanReqd = NO;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] syncCleanRequired:^(BOOL clean) {
|
||||
[rop syncCleanRequired:^(BOOL clean) {
|
||||
syncCleanReqd = clean;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
__block BOOL pushNotifications = NO;
|
||||
if ([[SNTConfigurator configurator] syncBaseURL]) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] pushNotifications:^(BOOL response) {
|
||||
[rop pushNotifications:^(BOOL response) {
|
||||
pushNotifications = response;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
|
||||
__block BOOL enableBundles = NO;
|
||||
if ([[SNTConfigurator configurator] syncBaseURL]) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] enableBundles:^(BOOL response) {
|
||||
[rop enableBundles:^(BOOL response) {
|
||||
enableBundles = response;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
|
||||
__block BOOL enableTransitiveRules = NO;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] enableTransitiveRules:^(BOOL response) {
|
||||
[rop enableTransitiveRules:^(BOOL response) {
|
||||
enableTransitiveRules = response;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
__block BOOL watchItemsEnabled = NO;
|
||||
@@ -163,20 +141,16 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
__block NSString *watchItemsPolicyVersion = nil;
|
||||
__block NSString *watchItemsConfigPath = nil;
|
||||
__block NSTimeInterval watchItemsLastUpdateEpoch = 0;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
watchItemsState:^(BOOL enabled, uint64_t ruleCount, NSString *policyVersion,
|
||||
NSString *configPath, NSTimeInterval lastUpdateEpoch) {
|
||||
watchItemsEnabled = enabled;
|
||||
if (enabled) {
|
||||
watchItemsRuleCount = ruleCount;
|
||||
watchItemsPolicyVersion = policyVersion;
|
||||
watchItemsConfigPath = configPath;
|
||||
watchItemsLastUpdateEpoch = lastUpdateEpoch;
|
||||
}
|
||||
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
[rop watchItemsState:^(BOOL enabled, uint64_t ruleCount, NSString *policyVersion,
|
||||
NSString *configPath, NSTimeInterval lastUpdateEpoch) {
|
||||
watchItemsEnabled = enabled;
|
||||
if (enabled) {
|
||||
watchItemsRuleCount = ruleCount;
|
||||
watchItemsPolicyVersion = policyVersion;
|
||||
watchItemsConfigPath = configPath;
|
||||
watchItemsLastUpdateEpoch = lastUpdateEpoch;
|
||||
}
|
||||
}];
|
||||
|
||||
// Wait a maximum of 5s for stats collected from daemon to arrive.
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
|
||||
@@ -205,6 +179,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"daemon" : @{
|
||||
@"driver_connected" : @(YES),
|
||||
@"mode" : clientMode ?: @"null",
|
||||
@"log_type" : eventLogType,
|
||||
@"file_logging" : @(fileLogging),
|
||||
@"watchdog_cpu_events" : @(cpuEvents),
|
||||
@"watchdog_ram_events" : @(ramEvents),
|
||||
@@ -265,6 +240,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
} else {
|
||||
printf(">>> Daemon Info\n");
|
||||
printf(" %-25s | %s\n", "Mode", [clientMode UTF8String]);
|
||||
printf(" %-25s | %s\n", "Log Type", [eventLogType UTF8String]);
|
||||
printf(" %-25s | %s\n", "File Logging", (fileLogging ? "Yes" : "No"));
|
||||
printf(" %-25s | %s\n", "USB Blocking", (configurator.blockUSBMount ? "Yes" : "No"));
|
||||
if (configurator.blockUSBMount && configurator.remountUSBMode.count > 0) {
|
||||
|
||||
@@ -137,6 +137,9 @@ objc_library(
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -342,6 +345,7 @@ objc_library(
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
@@ -409,6 +413,7 @@ objc_library(
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
":EndpointSecurityMessage",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
],
|
||||
@@ -425,6 +430,9 @@ objc_library(
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
":EndpointSecurityMessage",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -434,6 +442,7 @@ objc_library(
|
||||
hdrs = ["Logs/EndpointSecurity/Serializers/Empty.h"],
|
||||
deps = [
|
||||
":EndpointSecuritySerializer",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1098,6 +1107,7 @@ santa_unit_test(
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityClient",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -36,18 +37,22 @@ static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
|
||||
struct WatchItemPolicy {
|
||||
struct Process {
|
||||
Process(std::string bp, std::string sid, std::string ti,
|
||||
std::vector<uint8_t> cdh, std::string ch)
|
||||
std::vector<uint8_t> cdh, std::string ch, std::optional<bool> pb)
|
||||
: binary_path(bp),
|
||||
signing_id(sid),
|
||||
team_id(ti),
|
||||
cdhash(std::move(cdh)),
|
||||
certificate_sha256(ch) {}
|
||||
certificate_sha256(ch),
|
||||
platform_binary(pb) {}
|
||||
|
||||
bool operator==(const Process &other) const {
|
||||
return binary_path == other.binary_path &&
|
||||
signing_id == other.signing_id && team_id == other.team_id &&
|
||||
cdhash == other.cdhash &&
|
||||
certificate_sha256 == other.certificate_sha256;
|
||||
certificate_sha256 == other.certificate_sha256 &&
|
||||
platform_binary.has_value() == other.platform_binary.has_value() &&
|
||||
platform_binary.value_or(false) ==
|
||||
other.platform_binary.value_or(false);
|
||||
}
|
||||
|
||||
bool operator!=(const Process &other) const { return !(*this == other); }
|
||||
@@ -57,6 +62,7 @@ struct WatchItemPolicy {
|
||||
std::string team_id;
|
||||
std::vector<uint8_t> cdhash;
|
||||
std::string certificate_sha256;
|
||||
std::optional<bool> platform_binary;
|
||||
};
|
||||
|
||||
WatchItemPolicy(std::string_view n, std::string_view p,
|
||||
|
||||
@@ -45,6 +45,7 @@ extern NSString *const kWatchItemConfigKeyProcessesCertificateSha256;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesSigningID;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesTeamID;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesCDHash;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesPlatformBinary;
|
||||
|
||||
// Forward declarations
|
||||
namespace santa::santad::data_layer {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#include <ctype.h>
|
||||
#include <glob.h>
|
||||
#include <objc/NSObjCRuntime.h>
|
||||
#include <sys/syslimits.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -56,6 +55,7 @@ NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha
|
||||
NSString *const kWatchItemConfigKeyProcessesSigningID = @"SigningID";
|
||||
NSString *const kWatchItemConfigKeyProcessesTeamID = @"TeamID";
|
||||
NSString *const kWatchItemConfigKeyProcessesCDHash = @"CDHash";
|
||||
NSString *const kWatchItemConfigKeyProcessesPlatformBinary = @"PlatformBinary";
|
||||
|
||||
// https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/
|
||||
static constexpr NSUInteger kMaxTeamIDLength = 10;
|
||||
@@ -292,6 +292,8 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
|
||||
/// <string>AAAA</string>
|
||||
/// <key>TeamID</key>
|
||||
/// <string>BBBB</string>
|
||||
/// <key>PlatformBinary</key>
|
||||
/// <true/>
|
||||
/// </dict>
|
||||
/// <dict>
|
||||
/// <key>CertificateSha256</key>
|
||||
@@ -319,7 +321,9 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
false, HexValidator(CS_CDHASH_LEN * 2)) ||
|
||||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesCertificateSha256,
|
||||
[NSString class], err, false,
|
||||
HexValidator(CC_SHA256_DIGEST_LENGTH * 2))) {
|
||||
HexValidator(CC_SHA256_DIGEST_LENGTH * 2)) ||
|
||||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesPlatformBinary,
|
||||
[NSNumber class], err, false, nil)) {
|
||||
PopulateError(err, @"Failed to verify key content");
|
||||
return false;
|
||||
}
|
||||
@@ -329,7 +333,8 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
!process[kWatchItemConfigKeyProcessesSigningID] &&
|
||||
!process[kWatchItemConfigKeyProcessesTeamID] &&
|
||||
!process[kWatchItemConfigKeyProcessesCDHash] &&
|
||||
!process[kWatchItemConfigKeyProcessesCertificateSha256]) {
|
||||
!process[kWatchItemConfigKeyProcessesCertificateSha256] &&
|
||||
!process[kWatchItemConfigKeyProcessesPlatformBinary]) {
|
||||
PopulateError(err, @"No valid attributes set in process dictionary");
|
||||
return false;
|
||||
}
|
||||
@@ -340,7 +345,11 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
std::string([(process[kWatchItemConfigKeyProcessesTeamID] ?: @"") UTF8String]),
|
||||
HexStringToBytes(process[kWatchItemConfigKeyProcessesCDHash]),
|
||||
std::string(
|
||||
[(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @"") UTF8String])));
|
||||
[(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @"") UTF8String]),
|
||||
process[kWatchItemConfigKeyProcessesPlatformBinary]
|
||||
? std::make_optional(
|
||||
(bool)[process[kWatchItemConfigKeyProcessesPlatformBinary] boolValue])
|
||||
: std::nullopt));
|
||||
|
||||
return true;
|
||||
})) {
|
||||
@@ -421,6 +430,34 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsWatchItemNameValid(NSString *watch_item_name, NSError **err) {
|
||||
if (!watch_item_name) {
|
||||
// This shouldn't be possible as written, but handle just in case
|
||||
PopulateError(err, [NSString stringWithFormat:@"nil watch item name"]);
|
||||
return false;
|
||||
}
|
||||
|
||||
static dispatch_once_t once_token;
|
||||
static NSRegularExpression *regex;
|
||||
|
||||
dispatch_once(&once_token, ^{
|
||||
// Should only match legal C identifiers
|
||||
regex = [NSRegularExpression regularExpressionWithPattern:@"^[A-Za-z_][A-Za-z0-9_]*$"
|
||||
options:0
|
||||
error:nil];
|
||||
});
|
||||
|
||||
if ([regex numberOfMatchesInString:watch_item_name
|
||||
options:0
|
||||
range:NSMakeRange(0, watch_item_name.length)] != 1) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Key name must match regular expression \"%@\"",
|
||||
regex.pattern]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPolicy>> &policies,
|
||||
NSError **err) {
|
||||
if (![config[kWatchItemConfigKeyVersion] isKindOfClass:[NSString class]]) {
|
||||
@@ -454,9 +491,11 @@ bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPoli
|
||||
return false;
|
||||
}
|
||||
|
||||
if ([(NSString *)key length] == 0) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Invalid %@ key with length zero",
|
||||
kWatchItemConfigKeyWatchItems]);
|
||||
if (!IsWatchItemNameValid((NSString *)key, err)) {
|
||||
PopulateError(
|
||||
err, [NSString
|
||||
stringWithFormat:@"Invalid %@ key '%@': %@", kWatchItemConfigKeyWatchItems, key,
|
||||
(err && *err) ? (*err).localizedDescription : @"Unknown failure"]);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -584,6 +623,8 @@ void WatchItems::UpdateCurrentState(
|
||||
|
||||
last_update_time_ = [[NSDate date] timeIntervalSince1970];
|
||||
|
||||
LOGD(@"Changes to watch items detected, notifying registered clients.");
|
||||
|
||||
for (const id<SNTEndpointSecurityDynamicEventHandler> &client : registerd_clients_) {
|
||||
// Note: Enable clients on an async queue in case they perform any
|
||||
// synchronous work that could trigger ES events. Otherwise they might
|
||||
|
||||
@@ -51,6 +51,7 @@ namespace santa::santad::data_layer {
|
||||
|
||||
extern bool ParseConfig(NSDictionary *config,
|
||||
std::vector<std::shared_ptr<WatchItemPolicy>> &policies, NSError **err);
|
||||
extern bool IsWatchItemNameValid(NSString *watch_item_name, NSError **err);
|
||||
extern bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
std::vector<std::shared_ptr<WatchItemPolicy>> &policies,
|
||||
NSError **err);
|
||||
@@ -66,6 +67,7 @@ class WatchItemsPeer : public WatchItems {
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
|
||||
using santa::santad::data_layer::IsWatchItemNameValid;
|
||||
using santa::santad::data_layer::ParseConfig;
|
||||
using santa::santad::data_layer::ParseConfigSingleWatchItem;
|
||||
using santa::santad::data_layer::VerifyConfigWatchItemPaths;
|
||||
@@ -514,7 +516,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("mypath", "", "", {}, ""));
|
||||
WatchItemPolicy::Process("mypath", "", "", {}, "", std::nullopt));
|
||||
|
||||
// Test SigningID length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
@@ -533,7 +535,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "com.google.test", "", {}, ""));
|
||||
WatchItemPolicy::Process("", "com.google.test", "", {}, "", std::nullopt));
|
||||
|
||||
// Test TeamID length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
@@ -550,7 +552,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "", "myvalidtid", {}, ""));
|
||||
WatchItemPolicy::Process("", "", "myvalidtid", {}, "", std::nullopt));
|
||||
|
||||
// Test CDHash length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
@@ -577,7 +579,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "", "", cdhashBytes, ""));
|
||||
WatchItemPolicy::Process("", "", "", cdhashBytes, "", std::nullopt));
|
||||
|
||||
// Test Cert Hash length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
@@ -608,7 +610,22 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "", "", {}, [certHash UTF8String]));
|
||||
WatchItemPolicy::Process("", "", "", {}, [certHash UTF8String], std::nullopt));
|
||||
|
||||
// Test valid invalid PlatformBinary type
|
||||
proc_list = VerifyConfigWatchItemProcesses(
|
||||
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesPlatformBinary : @"YES"} ]},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
|
||||
// Test valid valid PlatformBinary
|
||||
proc_list = VerifyConfigWatchItemProcesses(
|
||||
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesPlatformBinary : @(YES)} ]},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "", "", {}, "", std::make_optional(true)));
|
||||
|
||||
// Test valid multiple attributes, multiple procs
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
@@ -619,6 +636,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
kWatchItemConfigKeyProcessesTeamID : @"validtid_1",
|
||||
kWatchItemConfigKeyProcessesCDHash : cdhash,
|
||||
kWatchItemConfigKeyProcessesCertificateSha256 : certHash,
|
||||
kWatchItemConfigKeyProcessesPlatformBinary : @(YES),
|
||||
},
|
||||
@{
|
||||
kWatchItemConfigKeyProcessesBinaryPath : @"mypath2",
|
||||
@@ -626,6 +644,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
kWatchItemConfigKeyProcessesTeamID : @"validtid_2",
|
||||
kWatchItemConfigKeyProcessesCDHash : cdhash,
|
||||
kWatchItemConfigKeyProcessesCertificateSha256 : certHash,
|
||||
kWatchItemConfigKeyProcessesPlatformBinary : @(NO),
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -634,10 +653,29 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 2);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("mypath1", "com.google.test1", "validtid_1", cdhashBytes,
|
||||
[certHash UTF8String]));
|
||||
[certHash UTF8String], std::make_optional(true)));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[1],
|
||||
WatchItemPolicy::Process("mypath2", "com.google.test2", "validtid_2", cdhashBytes,
|
||||
[certHash UTF8String]));
|
||||
[certHash UTF8String], std::make_optional(false)));
|
||||
}
|
||||
|
||||
- (void)testIsWatchItemNameValid {
|
||||
// Only legal C identifiers should be accepted
|
||||
XCTAssertFalse(IsWatchItemNameValid(nil, nil));
|
||||
XCTAssertFalse(IsWatchItemNameValid(@"", nil));
|
||||
XCTAssertFalse(IsWatchItemNameValid(@"1abc", nil));
|
||||
XCTAssertFalse(IsWatchItemNameValid(@"abc-1234", nil));
|
||||
XCTAssertFalse(IsWatchItemNameValid(@"a=b", nil));
|
||||
XCTAssertFalse(IsWatchItemNameValid(@"a!b", nil));
|
||||
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"_", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"_1", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"_1_", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"abc", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"A", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"A_B", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"FooName", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"bar_Name", nil));
|
||||
}
|
||||
|
||||
- (void)testParseConfig {
|
||||
@@ -670,16 +708,16 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"" : @{}}},
|
||||
policies, &err));
|
||||
XCTAssertFalse(
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"1" : @[]}},
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"a" : @[]}},
|
||||
policies, &err));
|
||||
XCTAssertFalse(
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"1" : @{}}},
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"a" : @{}}},
|
||||
policies, &err));
|
||||
|
||||
// Minimally successful config with watch item
|
||||
XCTAssertTrue(ParseConfig(@{
|
||||
kWatchItemConfigKeyVersion : @"1",
|
||||
kWatchItemConfigKeyWatchItems : @{@"1" : @{kWatchItemConfigKeyPaths : @[ @"asdf" ]}}
|
||||
kWatchItemConfigKeyWatchItems : @{@"a" : @{kWatchItemConfigKeyPaths : @[ @"asdf" ]}}
|
||||
},
|
||||
policies, &err));
|
||||
}
|
||||
@@ -753,8 +791,8 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
// Test multiple paths, options, and processes
|
||||
policies.clear();
|
||||
std::vector<WatchItemPolicy::Process> procs = {
|
||||
WatchItemPolicy::Process("pa", "", "", {}, ""),
|
||||
WatchItemPolicy::Process("pb", "", "", {}, ""),
|
||||
WatchItemPolicy::Process("pa", "", "", {}, "", std::nullopt),
|
||||
WatchItemPolicy::Process("pb", "", "", {}, "", std::nullopt),
|
||||
};
|
||||
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(@"rule", @{
|
||||
|
||||
@@ -89,30 +89,28 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
// There is a benign leak of the mock object in this test.
|
||||
// `handleMessage:recordEventMetrics:` will call `processMessage:handler:` in the parent
|
||||
// class. This will dispatch to two blocks and create message copies. The block that
|
||||
// handles `deadline` timeouts will not complete before the test finishes, and the
|
||||
// mock object will think that it has been leaked.
|
||||
::testing::Mock::AllowLeak(mockESApi.get());
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
execController:self.mockExecController
|
||||
compilerController:nil
|
||||
authResultCache:nullptr];
|
||||
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
// Test unhandled event type
|
||||
{
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
// There is a benign leak of the mock object in this test.
|
||||
// `handleMessage:recordEventMetrics:` will call `processMessage:handler:` in the parent
|
||||
// class. This will dispatch to two blocks and create message copies. The block that
|
||||
// handles `deadline` timeouts will not complete before the test finishes, and the
|
||||
// mock object will think that it has been leaked.
|
||||
::testing::Mock::AllowLeak(mockESApi.get());
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
execController:self.mockExecController
|
||||
compilerController:nil
|
||||
authResultCache:nullptr];
|
||||
|
||||
// Temporarily change the event type
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
|
||||
XCTAssertThrows([authClient handleMessage:Message(mockESApi, &esMsg)
|
||||
@@ -120,10 +118,25 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
XCTFail("Unhandled event types shouldn't call metrics recorder");
|
||||
}]);
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_EXEC;
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
// Test SNTExecutionController determines the event shouldn't be processed
|
||||
{
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
::testing::Mock::AllowLeak(mockESApi.get());
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
execController:self.mockExecController
|
||||
compilerController:nil
|
||||
authResultCache:nullptr];
|
||||
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
@@ -145,11 +158,28 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
|
||||
[mockAuthClient stopMocking];
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
// Test SNTExecutionController determines the event should be processed and
|
||||
// processMessage:handler: is called.
|
||||
{
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
::testing::Mock::AllowLeak(mockESApi.get());
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
execController:self.mockExecController
|
||||
compilerController:nil
|
||||
authResultCache:nullptr];
|
||||
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
@@ -169,11 +199,10 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
|
||||
[mockAuthClient stopMocking];
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
[mockAuthClient stopMocking];
|
||||
}
|
||||
|
||||
- (void)testProcessMessageWaitThenAllow {
|
||||
|
||||
@@ -49,6 +49,7 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
|
||||
@interface SNTEndpointSecurityClient ()
|
||||
@property int64_t deadlineMarginMS;
|
||||
@property SNTConfigurator *configurator;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityClient {
|
||||
@@ -68,6 +69,7 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
_esApi = std::move(esApi);
|
||||
_metrics = std::move(metrics);
|
||||
_deadlineMarginMS = 5000;
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
_processor = processor;
|
||||
|
||||
_authQueue = dispatch_queue_create(
|
||||
@@ -103,9 +105,8 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg
|
||||
ignoringOtherESClients:(BOOL)ignoringOtherESClients {
|
||||
if (esMsg->process->is_es_client && ignoringOtherESClients) {
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg {
|
||||
if (esMsg->process->is_es_client && [self.configurator ignoreOtherEndpointSecurityClients]) {
|
||||
if (esMsg->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true];
|
||||
}
|
||||
@@ -125,9 +126,7 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
self->_esClient = self->_esApi->NewClient(^(es_client_t *c, Message esMsg) {
|
||||
int64_t processingStart = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
es_event_type_t eventType = esMsg->event_type;
|
||||
if ([self shouldHandleMessage:esMsg
|
||||
ignoringOtherESClients:[[SNTConfigurator configurator]
|
||||
ignoreOtherEndpointSecurityClients]]) {
|
||||
if ([self shouldHandleMessage:esMsg]) {
|
||||
[self handleMessage:std::move(esMsg)
|
||||
recordEventMetrics:^(EventDisposition disposition) {
|
||||
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
@@ -46,8 +47,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result;
|
||||
- (void)handleMessage:(Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(santa::santad::EventDisposition disposition))recordEventMetrics;
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg
|
||||
ignoringOtherESClients:(BOOL)ignoringOtherESClients;
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg;
|
||||
|
||||
@property int64_t deadlineMarginMS;
|
||||
@end
|
||||
@@ -130,6 +130,9 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_ALLOW, true))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
id mockConfigurator = OCMStrictClassMock([SNTConfigurator class]);
|
||||
OCMStub([mockConfigurator configurator]).andReturn(mockConfigurator);
|
||||
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
@@ -139,24 +142,31 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
// Is ES client, but don't ignore others == Should Handle
|
||||
OCMExpect([mockConfigurator ignoreOtherEndpointSecurityClients]).andReturn(NO);
|
||||
esMsg.process->is_es_client = true;
|
||||
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:NO]);
|
||||
XCTAssertTrue([client shouldHandleMessage:msg]);
|
||||
|
||||
// Not ES client, but ignore others == Should Handle
|
||||
// Don't setup configurator mock since it won't be called when `is_es_client` is false
|
||||
esMsg.process->is_es_client = false;
|
||||
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
XCTAssertTrue([client shouldHandleMessage:msg]);
|
||||
|
||||
// Is ES client, don't ignore others, and non-AUTH == Don't Handle
|
||||
OCMExpect([mockConfigurator ignoreOtherEndpointSecurityClients]).andReturn(YES);
|
||||
esMsg.process->is_es_client = true;
|
||||
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
XCTAssertFalse([client shouldHandleMessage:msg]);
|
||||
|
||||
// Is ES client, don't ignore others, and AUTH == Respond and Don't Handle
|
||||
OCMExpect([mockConfigurator ignoreOtherEndpointSecurityClients]).andReturn(YES);
|
||||
esMsg.process->is_es_client = true;
|
||||
esMsg.action_type = ES_ACTION_TYPE_AUTH;
|
||||
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
XCTAssertFalse([client shouldHandleMessage:msg]);
|
||||
}
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockConfigurator));
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
[mockConfigurator stopMocking];
|
||||
}
|
||||
|
||||
- (void)testPopulateAuditTokenSelf {
|
||||
@@ -359,6 +369,8 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
- (void)testRespondToMessageWithAuthResultCacheable {
|
||||
es_message_t esMsg;
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_EXEC;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "Source/common/Platform.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/common/SantaVnode.h"
|
||||
#include "Source/common/SantaVnodeHash.h"
|
||||
@@ -190,6 +191,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizer ()
|
||||
@property SNTDecisionCache *decisionCache;
|
||||
@property bool isSubscribed;
|
||||
@property SNTMetricBooleanGauge *famEnabled;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityFileAccessAuthorizer {
|
||||
@@ -218,6 +220,11 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
|
||||
_decisionCache = decisionCache;
|
||||
|
||||
_famEnabled = [[SNTMetricSet sharedInstance]
|
||||
booleanGaugeWithName:@"/santa/fam_enabled"
|
||||
fieldNames:@[]
|
||||
helpText:@"Whether or not the FAM client is enabled"];
|
||||
|
||||
[self establishClientOrDie];
|
||||
|
||||
[super enableTargetPathWatching];
|
||||
@@ -319,14 +326,24 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
// configured process exception while the check for a valid code signature
|
||||
// is more broad and applies whether or not process exceptions exist.
|
||||
if (esProc->codesigning_flags & CS_SIGNED) {
|
||||
// Check if the instigating process has an allowed TeamID
|
||||
if (!policyProc.team_id.empty() && esProc->team_id.data &&
|
||||
policyProc.team_id != esProc->team_id.data) {
|
||||
// Check whether or not the process is a platform binary if specified by the policy.
|
||||
if (policyProc.platform_binary.has_value() &&
|
||||
policyProc.platform_binary.value() != esProc->is_platform_binary) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!policyProc.signing_id.empty() && esProc->signing_id.data &&
|
||||
policyProc.signing_id != esProc->signing_id.data) {
|
||||
// If the policy contains a team ID, check that the instigating process
|
||||
// also has a team ID and matches the policy.
|
||||
if (!policyProc.team_id.empty() &&
|
||||
(!esProc->team_id.data || (policyProc.team_id != esProc->team_id.data))) {
|
||||
// We expected a team ID to match against, but the process didn't have one.
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the policy contains a signing ID, check that the instigating process
|
||||
// also has a signing ID and matches the policy.
|
||||
if (!policyProc.signing_id.empty() &&
|
||||
(!esProc->signing_id.data || (policyProc.signing_id != esProc->signing_id.data))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -512,15 +529,21 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
#endif
|
||||
|
||||
if (!self.isSubscribed) {
|
||||
self.isSubscribed = [super subscribe:events];
|
||||
[super clearCache];
|
||||
if ([super subscribe:events]) {
|
||||
self.isSubscribed = true;
|
||||
[self.famEnabled set:YES forFieldValues:@[]];
|
||||
}
|
||||
}
|
||||
|
||||
// Always clear cache to ensure operations that were previously allowed are re-evaluated.
|
||||
[super clearCache];
|
||||
}
|
||||
|
||||
- (void)disable {
|
||||
if (self.isSubscribed) {
|
||||
if ([super unsubscribeAll]) {
|
||||
self.isSubscribed = false;
|
||||
[self.famEnabled set:NO forFieldValues:@[]];
|
||||
}
|
||||
[super unmuteEverything];
|
||||
}
|
||||
|
||||
@@ -384,6 +384,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
esProc.codesigning_flags = CS_SIGNED;
|
||||
esProc.team_id = MakeESStringToken(teamId);
|
||||
esProc.signing_id = MakeESStringToken(signingId);
|
||||
esProc.is_platform_binary = true;
|
||||
std::memcpy(esProc.cdhash, cdhashBytes.data(), sizeof(esProc.cdhash));
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
@@ -405,7 +406,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(@(instigatingCertHash));
|
||||
|
||||
WatchItemPolicy::Process policyProc("", "", "", {}, "");
|
||||
WatchItemPolicy::Process policyProc("", "", "", {}, "", std::nullopt);
|
||||
|
||||
{
|
||||
// Process policy matching single attribute - path
|
||||
@@ -423,6 +424,11 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertTrue([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
policyProc.signing_id = "badid";
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
es_process_t esProcEmptySigningID = MakeESProcess(&esFile);
|
||||
esProcEmptySigningID.codesigning_flags = CS_SIGNED;
|
||||
esProcEmptySigningID.team_id.data = NULL;
|
||||
esProcEmptySigningID.team_id.length = 0;
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProcEmptySigningID]);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -432,6 +438,11 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertTrue([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
policyProc.team_id = "badid";
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
es_process_t esProcEmptyTeamID = MakeESProcess(&esFile);
|
||||
esProcEmptyTeamID.codesigning_flags = CS_SIGNED;
|
||||
esProcEmptyTeamID.signing_id.data = NULL;
|
||||
esProcEmptyTeamID.signing_id.length = 0;
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProcEmptyTeamID]);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -452,6 +463,15 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
}
|
||||
|
||||
{
|
||||
// Process policy matching single attribute - platform binary
|
||||
ClearWatchItemPolicyProcess(policyProc);
|
||||
policyProc.platform_binary = std::make_optional(true);
|
||||
XCTAssertTrue([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
policyProc.platform_binary = std::make_optional(false);
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
}
|
||||
|
||||
{
|
||||
// Process policy with only a subset of matching attributes
|
||||
ClearWatchItemPolicyProcess(policyProc);
|
||||
@@ -476,7 +496,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
const char *instigatingPath = "/path/to/proc";
|
||||
const char *instigatingTeamID = "my_teamid";
|
||||
const char *instigatingCertHash = "abc123";
|
||||
WatchItemPolicy::Process policyProc(instigatingPath, "", "", {}, "");
|
||||
WatchItemPolicy::Process policyProc(instigatingPath, "", "", {}, "", std::nullopt);
|
||||
std::array<uint8_t, 20> instigatingCDHash;
|
||||
instigatingCDHash.fill(0x41);
|
||||
es_file_t esFile = MakeESFile(instigatingPath);
|
||||
|
||||
@@ -65,6 +65,8 @@ class Logger {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedProcess &enriched_process,
|
||||
const std::string &target, FileAccessPolicyDecision decision);
|
||||
|
||||
void Flush();
|
||||
|
||||
friend class santa::santad::logs::endpoint_security::LoggerPeer;
|
||||
|
||||
private:
|
||||
|
||||
@@ -107,4 +107,8 @@ void Logger::LogFileAccess(
|
||||
enriched_process, target, decision));
|
||||
}
|
||||
|
||||
void Logger::Flush() {
|
||||
writer_->Flush();
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
|
||||
@@ -41,7 +42,8 @@ class BasicString : public Serializer {
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &,
|
||||
SNTCachedDecision *) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
|
||||
@@ -241,12 +241,12 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExchange &msg)
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg) {
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg, SNTCachedDecision *cd) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString(1024); // EXECs tend to be bigger, reserve more space.
|
||||
|
||||
SNTCachedDecision *cd =
|
||||
[[SNTDecisionCache sharedCache] cachedDecisionForFile:esm.event.exec.target->executable->stat];
|
||||
// Only need to grab the shared instance once
|
||||
static SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
str.append("action=EXEC|decision=");
|
||||
str.append(GetDecisionString(cd.decision));
|
||||
@@ -291,7 +291,7 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg) {
|
||||
msg.instigator().real_group());
|
||||
|
||||
str.append("|mode=");
|
||||
str.append(GetModeString([[SNTConfigurator configurator] clientMode]));
|
||||
str.append(GetModeString([configurator clientMode]));
|
||||
str.append("|path=");
|
||||
str.append(FilePath(esm.event.exec.target->executable).Sanitized());
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
OCMStub([self.mockDecisionCache cachedDecisionForFile:{}])
|
||||
OCMStub([self.mockDecisionCache resetTimestampForCachedDecision:{}])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(self.testCachedDecision);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
@@ -33,7 +34,8 @@ class Empty : public Serializer {
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &,
|
||||
SNTCachedDecision *) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
|
||||
@@ -39,7 +39,7 @@ std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExchange &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExec &msg) {
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExec &msg, SNTCachedDecision *cd) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace es = santa::santad::event_providers::endpoint_security;
|
||||
int fake;
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedClose *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExchange *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExec *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExec *)&fake, nil).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExit *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedFork *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedLink *)&fake).size(), 0);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/common/santa_proto_include_wrapper.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
@@ -40,7 +41,8 @@ class Protobuf : public Serializer {
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &,
|
||||
SNTCachedDecision *) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
|
||||
@@ -384,7 +384,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
|
||||
std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
|
||||
std::vector<uint8_t> vec(santa_msg->ByteSizeLong());
|
||||
santa_msg->SerializeToArray(vec.data(), (int)vec.capacity());
|
||||
santa_msg->SerializeWithCachedSizesToArray(vec.data());
|
||||
return vec;
|
||||
}
|
||||
|
||||
@@ -418,12 +418,12 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExchange &msg) {
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg) {
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCachedDecision *cd) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
SNTCachedDecision *cd = [[SNTDecisionCache sharedCache]
|
||||
cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
|
||||
// Only need to grab the shared instance once
|
||||
static SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
GetDecisionEnum(cd.decision);
|
||||
|
||||
@@ -444,28 +444,37 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg) {
|
||||
}
|
||||
|
||||
uint32_t arg_count = esapi_->ExecArgCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < arg_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecArg(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_args(tok.data, tok.length);
|
||||
if (arg_count > 0) {
|
||||
pb_exec->mutable_args()->Reserve(arg_count);
|
||||
for (uint32_t i = 0; i < arg_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecArg(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_args(tok.data, tok.length);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t env_count = esapi_->ExecEnvCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < env_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecEnv(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_envs(tok.data, tok.length);
|
||||
if (env_count > 0) {
|
||||
pb_exec->mutable_envs()->Reserve(env_count);
|
||||
for (uint32_t i = 0; i < env_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecEnv(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_envs(tok.data, tok.length);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.es_msg().version >= 4) {
|
||||
int32_t max_fd = -1;
|
||||
uint32_t fd_count = esapi_->ExecFDCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < fd_count; i++) {
|
||||
const es_fd_t *fd = esapi_->ExecFD(&msg.es_msg().event.exec, i);
|
||||
max_fd = std::max(max_fd, fd->fd);
|
||||
::pbv1::FileDescriptor *pb_fd = pb_exec->add_fds();
|
||||
pb_fd->set_fd(fd->fd);
|
||||
pb_fd->set_fd_type(GetFileDescriptorType(fd->fdtype));
|
||||
if (fd->fdtype == PROX_FDTYPE_PIPE) {
|
||||
pb_fd->set_pipe_id(fd->pipe.pipe_id);
|
||||
if (fd_count > 0) {
|
||||
pb_exec->mutable_fds()->Reserve(fd_count);
|
||||
for (uint32_t i = 0; i < fd_count; i++) {
|
||||
const es_fd_t *fd = esapi_->ExecFD(&msg.es_msg().event.exec, i);
|
||||
max_fd = std::max(max_fd, fd->fd);
|
||||
::pbv1::FileDescriptor *pb_fd = pb_exec->add_fds();
|
||||
pb_fd->set_fd(fd->fd);
|
||||
pb_fd->set_fd_type(GetFileDescriptorType(fd->fdtype));
|
||||
if (fd->fdtype == PROX_FDTYPE_PIPE) {
|
||||
pb_fd->set_pipe_id(fd->pipe.pipe_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,7 +485,7 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg) {
|
||||
|
||||
pb_exec->set_decision(GetDecisionEnum(cd.decision));
|
||||
pb_exec->set_reason(GetReasonEnum(cd.decision));
|
||||
pb_exec->set_mode(GetModeEnum([[SNTConfigurator configurator] clientMode]));
|
||||
pb_exec->set_mode(GetModeEnum([configurator clientMode]));
|
||||
|
||||
if (cd.certSHA256 || cd.certCommonName) {
|
||||
EncodeCertificateInfo(pb_exec->mutable_certificate_info(), cd.certSHA256, cd.certCommonName);
|
||||
|
||||
@@ -283,7 +283,7 @@ void SerializeAndCheckNonESEvents(
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
OCMStub([self.mockDecisionCache cachedDecisionForFile:{}])
|
||||
OCMStub([self.mockDecisionCache resetTimestampForCachedDecision:{}])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(self.testCachedDecision);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SERIALIZER_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SERIALIZER_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
|
||||
@@ -46,7 +47,8 @@ class Serializer {
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) = 0;
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &,
|
||||
SNTCachedDecision *cd) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
|
||||
@@ -46,15 +46,20 @@ std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExch
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExec &msg) {
|
||||
static SNTDecisionCache *decision_cache = [SNTDecisionCache sharedCache];
|
||||
|
||||
SNTCachedDecision *cd;
|
||||
const es_message_t &es_msg = msg.es_msg();
|
||||
if (es_msg.action_type == ES_ACTION_TYPE_NOTIFY &&
|
||||
es_msg.action.notify.result.auth == ES_AUTH_RESULT_ALLOW) {
|
||||
// For allowed execs, cached decision timestamps must be updated
|
||||
[[SNTDecisionCache sharedCache]
|
||||
cd = [decision_cache
|
||||
resetTimestampForCachedDecision:msg.es_msg().event.exec.target->executable->stat];
|
||||
} else {
|
||||
cd = [decision_cache cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
|
||||
}
|
||||
|
||||
return SerializeMessage(msg);
|
||||
return SerializeMessage(msg, cd);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExit &msg) {
|
||||
return SerializeMessage(msg);
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
|
||||
#include "Source/common/SantaCache.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
#include "Source/common/SantaVnodeHash.h"
|
||||
|
||||
// These functions are exported by the Security framework, but are not included in headers
|
||||
extern "C" Boolean SecTranslocateIsTranslocatedURL(CFURLRef path, bool *isTranslocated,
|
||||
CFErrorRef *__nullable error);
|
||||
@@ -32,10 +36,17 @@ static inline void SetThreadIDs(uid_t uid, gid_t gid) {
|
||||
}
|
||||
|
||||
NSString *OriginalPathForTranslocation(const es_process_t *es_proc) {
|
||||
// Cache vnodes that have been determined to not be translocated
|
||||
static SantaCache<SantaVnode, bool> isNotTranslocatedCache(1024);
|
||||
|
||||
if (!es_proc) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (isNotTranslocatedCache.get(SantaVnode::VnodeForFile(es_proc->executable))) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Note: Benchmarks showed better performance using `URLWithString` with a `file://` prefix
|
||||
// compared to using `fileURLWithPath`.
|
||||
CFURLRef cfExecURL = (__bridge CFURLRef)
|
||||
@@ -58,6 +69,8 @@ NSString *OriginalPathForTranslocation(const es_process_t *es_proc) {
|
||||
if (dropPrivs) {
|
||||
SetThreadIDs(KAUTH_UID_NONE, KAUTH_GID_NONE);
|
||||
}
|
||||
} else {
|
||||
isNotTranslocatedCache.set(SantaVnode::VnodeForFile(es_proc->executable), true);
|
||||
}
|
||||
|
||||
return [origURL path];
|
||||
|
||||
@@ -42,17 +42,18 @@ class File : public Writer, public std::enable_shared_from_this<File> {
|
||||
~File();
|
||||
|
||||
void Write(std::vector<uint8_t> &&bytes) override;
|
||||
void Flush() override;
|
||||
|
||||
friend class santa::santad::logs::endpoint_security::writers::FilePeer;
|
||||
|
||||
private:
|
||||
void OpenFileHandle();
|
||||
void OpenFileHandleLocked();
|
||||
void WatchLogFile();
|
||||
void FlushBuffer();
|
||||
void FlushLocked();
|
||||
bool ShouldFlush();
|
||||
|
||||
void EnsureCapacity(size_t additional_bytes);
|
||||
void CopyData(const std::vector<uint8_t> &bytes);
|
||||
void EnsureCapacityLocked(size_t additional_bytes);
|
||||
void CopyDataLocked(const std::vector<uint8_t> &bytes);
|
||||
|
||||
std::vector<uint8_t> buffer_;
|
||||
size_t batch_size_bytes_;
|
||||
|
||||
@@ -39,7 +39,7 @@ std::shared_ptr<File> File::Create(NSString *path, uint64_t flush_timeout_ms,
|
||||
if (!shared_writer) {
|
||||
return;
|
||||
}
|
||||
shared_writer->FlushBuffer();
|
||||
shared_writer->FlushLocked();
|
||||
});
|
||||
|
||||
dispatch_resume(ret_writer->timer_source_);
|
||||
@@ -55,7 +55,7 @@ File::File(NSString *path, size_t batch_size_bytes, size_t max_expected_write_si
|
||||
timer_source_(timer_source),
|
||||
watch_source_(nullptr) {
|
||||
path_ = path;
|
||||
OpenFileHandle();
|
||||
OpenFileHandleLocked();
|
||||
}
|
||||
|
||||
void File::WatchLogFile() {
|
||||
@@ -69,7 +69,7 @@ void File::WatchLogFile() {
|
||||
auto shared_this = shared_from_this();
|
||||
dispatch_source_set_event_handler(watch_source_, ^{
|
||||
[shared_this->file_handle_ closeFile];
|
||||
shared_this->OpenFileHandle();
|
||||
shared_this->OpenFileHandleLocked();
|
||||
shared_this->WatchLogFile();
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ File::~File() {
|
||||
}
|
||||
|
||||
// IMPORTANT: Not thread safe.
|
||||
void File::OpenFileHandle() {
|
||||
void File::OpenFileHandleLocked() {
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
if (![fm fileExistsAtPath:path_]) {
|
||||
[fm createFileAtPath:path_ contents:nil attributes:nil];
|
||||
@@ -101,10 +101,10 @@ void File::Write(std::vector<uint8_t> &&bytes) {
|
||||
dispatch_async(q_, ^{
|
||||
std::vector<uint8_t> moved_bytes = std::move(temp_bytes);
|
||||
|
||||
shared_this->CopyData(moved_bytes);
|
||||
shared_this->CopyDataLocked(moved_bytes);
|
||||
|
||||
if (shared_this->ShouldFlush()) {
|
||||
shared_this->FlushBuffer();
|
||||
shared_this->FlushLocked();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -113,22 +113,28 @@ bool File::ShouldFlush() {
|
||||
return buffer_offset_ >= batch_size_bytes_;
|
||||
}
|
||||
|
||||
void File::Flush() {
|
||||
dispatch_sync(q_, ^{
|
||||
FlushLocked();
|
||||
});
|
||||
}
|
||||
|
||||
// IMPORTANT: Not thread safe.
|
||||
void File::EnsureCapacity(size_t additional_bytes) {
|
||||
void File::EnsureCapacityLocked(size_t additional_bytes) {
|
||||
if ((buffer_offset_ + additional_bytes) > buffer_.capacity()) {
|
||||
buffer_.resize(buffer_.capacity() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: Not thread safe.
|
||||
void File::CopyData(const std::vector<uint8_t> &bytes) {
|
||||
EnsureCapacity(bytes.size());
|
||||
void File::CopyDataLocked(const std::vector<uint8_t> &bytes) {
|
||||
EnsureCapacityLocked(bytes.size());
|
||||
std::copy(bytes.begin(), bytes.end(), buffer_.begin() + buffer_offset_);
|
||||
buffer_offset_ += bytes.size();
|
||||
}
|
||||
|
||||
// IMPORTANT: Not thread safe.
|
||||
void File::FlushBuffer() {
|
||||
void File::FlushLocked() {
|
||||
if (likely(buffer_offset_ > 0)) {
|
||||
write(file_handle_.fileDescriptor, buffer_.data(), buffer_offset_);
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ class FilePeer : public File {
|
||||
// Make constructors visible
|
||||
using File::File;
|
||||
|
||||
using File::CopyData;
|
||||
using File::EnsureCapacity;
|
||||
using File::CopyDataLocked;
|
||||
using File::EnsureCapacityLocked;
|
||||
using File::ShouldFlush;
|
||||
using File::WatchLogFile;
|
||||
|
||||
@@ -192,7 +192,7 @@ bool WaitForBufferSize(std::shared_ptr<FilePeer> file, size_t expectedSize) {
|
||||
XCTAssertEqual(file->InternalBufferSize(), 0);
|
||||
XCTAssertEqual(file->InternalBufferCapacity(), initialCapacity);
|
||||
|
||||
file->EnsureCapacity(batchSize);
|
||||
file->EnsureCapacityLocked(batchSize);
|
||||
|
||||
// No data was written, so size is still 0
|
||||
XCTAssertEqual(file->InternalBufferSize(), 0);
|
||||
@@ -201,7 +201,7 @@ bool WaitForBufferSize(std::shared_ptr<FilePeer> file, size_t expectedSize) {
|
||||
// the initial amount
|
||||
XCTAssertEqual(file->InternalBufferCapacity(), initialCapacity);
|
||||
|
||||
file->EnsureCapacity(initialCapacity + 100);
|
||||
file->EnsureCapacityLocked(initialCapacity + 100);
|
||||
|
||||
// No data was written, so size is still 0
|
||||
XCTAssertEqual(file->InternalBufferSize(), 0);
|
||||
@@ -225,15 +225,15 @@ bool WaitForBufferSize(std::shared_ptr<FilePeer> file, size_t expectedSize) {
|
||||
XCTAssertEqual(file->InternalBufferSize(), 0);
|
||||
XCTAssertEqual(file->InternalBufferCapacity(), initialCapacity);
|
||||
|
||||
file->CopyData(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
|
||||
// After a copy, buffer size should match copied data size
|
||||
XCTAssertEqual(file->InternalBufferSize(), bytes.size());
|
||||
|
||||
// Do a couple more copies that should require the buffer to grow and then
|
||||
// confirm the size/capacity still matches expectations
|
||||
file->CopyData(bytes);
|
||||
file->CopyData(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
XCTAssertEqual(file->InternalBufferSize(), bytes.size() * 3);
|
||||
XCTAssertEqual(file->InternalBufferCapacity(), initialCapacity * 2);
|
||||
}
|
||||
@@ -249,7 +249,7 @@ bool WaitForBufferSize(std::shared_ptr<FilePeer> file, size_t expectedSize) {
|
||||
XCTAssertFalse(file->ShouldFlush());
|
||||
|
||||
// Copy some data into the buffer
|
||||
file->CopyData(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
|
||||
// Buffer size should be updated
|
||||
XCTAssertEqual(file->InternalBufferSize(), bytes.size());
|
||||
@@ -258,8 +258,8 @@ bool WaitForBufferSize(std::shared_ptr<FilePeer> file, size_t expectedSize) {
|
||||
XCTAssertFalse(file->ShouldFlush());
|
||||
|
||||
// Exceed the batch size
|
||||
file->CopyData(bytes);
|
||||
file->CopyData(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
|
||||
// Should want to flush now that the batch size is exceeded
|
||||
XCTAssertTrue(file->ShouldFlush());
|
||||
|
||||
@@ -28,6 +28,7 @@ class Null : public Writer {
|
||||
static std::shared_ptr<Null> Create();
|
||||
|
||||
void Write(std::vector<uint8_t>&& bytes) override;
|
||||
void Flush() override;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
@@ -24,4 +24,8 @@ void Null::Write(std::vector<uint8_t> &&bytes) {
|
||||
// Intentionally do nothing
|
||||
}
|
||||
|
||||
void Null::Flush() {
|
||||
// Intentionally do nothing
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
@@ -46,7 +46,7 @@ class Spool : public Writer, public std::enable_shared_from_this<Spool> {
|
||||
~Spool();
|
||||
|
||||
void Write(std::vector<uint8_t> &&bytes) override;
|
||||
bool Flush();
|
||||
void Flush() override;
|
||||
|
||||
void BeginFlushTask();
|
||||
|
||||
@@ -54,6 +54,8 @@ class Spool : public Writer, public std::enable_shared_from_this<Spool> {
|
||||
friend class santa::santad::logs::endpoint_security::writers::SpoolPeer;
|
||||
|
||||
private:
|
||||
bool FlushLocked();
|
||||
|
||||
dispatch_queue_t q_ = NULL;
|
||||
dispatch_source_t timer_source_ = NULL;
|
||||
::fsspool::FsSpoolWriter spool_writer_;
|
||||
|
||||
@@ -81,7 +81,7 @@ void Spool::BeginFlushTask() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shared_writer->Flush()) {
|
||||
if (!shared_writer->FlushLocked()) {
|
||||
LOGE(@"Spool writer: periodic flush failed.");
|
||||
}
|
||||
|
||||
@@ -94,7 +94,13 @@ void Spool::BeginFlushTask() {
|
||||
flush_task_started_ = true;
|
||||
}
|
||||
|
||||
bool Spool::Flush() {
|
||||
void Spool::Flush() {
|
||||
dispatch_sync(q_, ^{
|
||||
FlushLocked();
|
||||
});
|
||||
}
|
||||
|
||||
bool Spool::FlushLocked() {
|
||||
if (log_batch_writer_.Flush().ok()) {
|
||||
accumulated_bytes_ = 0;
|
||||
return true;
|
||||
@@ -122,7 +128,7 @@ void Spool::Write(std::vector<uint8_t> &&bytes) {
|
||||
any.set_type_url(type_url_);
|
||||
|
||||
if (shared_this->accumulated_bytes_ >= shared_this->spool_file_size_threshold_) {
|
||||
shared_this->Flush();
|
||||
shared_this->FlushLocked();
|
||||
}
|
||||
|
||||
// Only write the new message if we have room left.
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace santa::santad::logs::endpoint_security::writers {
|
||||
class SpoolPeer : public Spool {
|
||||
public:
|
||||
// Make constructors visible
|
||||
using Spool::FlushLocked;
|
||||
using Spool::Spool;
|
||||
|
||||
std::string GetTypeUrl() { return type_url_; }
|
||||
@@ -123,7 +124,7 @@ using santa::santad::logs::endpoint_security::writers::SpoolPeer;
|
||||
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 0);
|
||||
|
||||
// Manual Flush
|
||||
XCTAssertTrue(spool->Flush());
|
||||
XCTAssertTrue(spool->FlushLocked());
|
||||
|
||||
// A new log entry should exist
|
||||
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 1);
|
||||
|
||||
@@ -26,6 +26,7 @@ class Syslog : public Writer {
|
||||
static std::shared_ptr<Syslog> Create();
|
||||
|
||||
void Write(std::vector<uint8_t>&& bytes) override;
|
||||
void Flush() override;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
@@ -26,4 +26,8 @@ void Syslog::Write(std::vector<uint8_t> &&bytes) {
|
||||
os_log(OS_LOG_DEFAULT, "%{public}s", bytes.data());
|
||||
}
|
||||
|
||||
void Syslog::Flush() {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
@@ -24,6 +24,7 @@ class Writer {
|
||||
virtual ~Writer() = default;
|
||||
|
||||
virtual void Write(std::vector<uint8_t>&& bytes) = 0;
|
||||
virtual void Flush() = 0;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
@@ -49,11 +49,11 @@ using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
|
||||
|
||||
class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
public:
|
||||
static std::shared_ptr<Metrics> Create(SNTMetricSet *metricSet, uint64_t interval);
|
||||
static std::shared_ptr<Metrics> Create(SNTMetricSet *metric_set, uint64_t interval);
|
||||
|
||||
Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
void (^run_on_first_start)(Metrics *));
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *));
|
||||
|
||||
~Metrics();
|
||||
|
||||
@@ -62,7 +62,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
void StopPoll();
|
||||
void SetInterval(uint64_t interval);
|
||||
|
||||
void FlushMetrics();
|
||||
// Force an immediate flush and export of metrics
|
||||
void Export();
|
||||
|
||||
void SetEventMetrics(Processor processor, es_event_type_t event_type,
|
||||
EventDisposition disposition, int64_t nanos);
|
||||
@@ -70,16 +71,20 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
friend class santa::santad::MetricsPeer;
|
||||
|
||||
private:
|
||||
void FlushMetrics();
|
||||
void ExportLocked(SNTMetricSet *metric_set);
|
||||
|
||||
MOLXPCConnection *metrics_connection_;
|
||||
dispatch_queue_t q_;
|
||||
dispatch_source_t timer_source_;
|
||||
uint64_t interval_;
|
||||
SNTMetricInt64Gauge *event_processing_times_;
|
||||
SNTMetricCounter *event_counts_;
|
||||
SNTMetricSet *metric_set_;
|
||||
// Tracks whether or not the timer_source should be running.
|
||||
// This helps manage dispatch source state to ensure the source is not
|
||||
// suspended, resumed, or cancelled while in an improper state.
|
||||
bool running_;
|
||||
bool running_ = false;
|
||||
void (^run_on_first_start_)(Metrics *);
|
||||
|
||||
// Separate queue used for setting event metrics
|
||||
|
||||
@@ -108,27 +108,28 @@ NSString *const EventDispositionToString(EventDisposition d) {
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metricSet, uint64_t interval) {
|
||||
std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t interval) {
|
||||
dispatch_queue_t q = dispatch_queue_create("com.google.santa.santametricsservice.q",
|
||||
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
|
||||
|
||||
dispatch_source_t timer_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
|
||||
|
||||
SNTMetricInt64Gauge *event_processing_times =
|
||||
[metricSet int64GaugeWithName:@"/santa/event_processing_time"
|
||||
fieldNames:@[ @"Processor", @"Event" ]
|
||||
helpText:@"Time to process various event types by each processor"];
|
||||
[metric_set int64GaugeWithName:@"/santa/event_processing_time"
|
||||
fieldNames:@[ @"Processor", @"Event" ]
|
||||
helpText:@"Time to process various event types by each processor"];
|
||||
|
||||
SNTMetricCounter *event_counts =
|
||||
[metricSet counterWithName:@"/santa/event_count"
|
||||
fieldNames:@[ @"Processor", @"Event", @"Disposition" ]
|
||||
helpText:@"Events received and processed by each processor"];
|
||||
[metric_set counterWithName:@"/santa/event_count"
|
||||
fieldNames:@[ @"Processor", @"Event", @"Disposition" ]
|
||||
helpText:@"Events received and processed by each processor"];
|
||||
|
||||
std::shared_ptr<Metrics> metrics = std::make_shared<Metrics>(
|
||||
q, timer_source, interval, event_processing_times, event_counts, ^(Metrics *metrics) {
|
||||
SNTRegisterCoreMetrics();
|
||||
metrics->EstablishConnection();
|
||||
});
|
||||
std::shared_ptr<Metrics> metrics =
|
||||
std::make_shared<Metrics>(q, timer_source, interval, event_processing_times, event_counts,
|
||||
metric_set, ^(Metrics *metrics) {
|
||||
SNTRegisterCoreMetrics();
|
||||
metrics->EstablishConnection();
|
||||
});
|
||||
|
||||
std::weak_ptr<Metrics> weak_metrics(metrics);
|
||||
dispatch_source_set_event_handler(metrics->timer_source_, ^{
|
||||
@@ -137,10 +138,7 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metricSet, uint64_t inter
|
||||
return;
|
||||
}
|
||||
|
||||
shared_metrics->FlushMetrics();
|
||||
|
||||
[[shared_metrics->metrics_connection_ remoteObjectProxy]
|
||||
exportForMonitoring:[metricSet export]];
|
||||
shared_metrics->ExportLocked(metric_set);
|
||||
});
|
||||
|
||||
return metrics;
|
||||
@@ -148,13 +146,13 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metricSet, uint64_t inter
|
||||
|
||||
Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
void (^run_on_first_start)(Metrics *))
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *))
|
||||
: q_(q),
|
||||
timer_source_(timer_source),
|
||||
interval_(interval),
|
||||
event_processing_times_(event_processing_times),
|
||||
event_counts_(event_counts),
|
||||
running_(false),
|
||||
metric_set_(metric_set),
|
||||
run_on_first_start_(run_on_first_start) {
|
||||
SetInterval(interval_);
|
||||
|
||||
@@ -184,6 +182,17 @@ void Metrics::EstablishConnection() {
|
||||
metrics_connection_ = metrics_connection;
|
||||
}
|
||||
|
||||
void Metrics::Export() {
|
||||
dispatch_sync(q_, ^{
|
||||
ExportLocked(metric_set_);
|
||||
});
|
||||
}
|
||||
|
||||
void Metrics::ExportLocked(SNTMetricSet *metric_set) {
|
||||
FlushMetrics();
|
||||
[[metrics_connection_ remoteObjectProxy] exportForMonitoring:[metric_set export]];
|
||||
}
|
||||
|
||||
void Metrics::FlushMetrics() {
|
||||
dispatch_sync(events_q_, ^{
|
||||
for (const auto &kv : event_counts_cache_) {
|
||||
|
||||
@@ -39,6 +39,7 @@ extern NSString *const EventDispositionToString(EventDisposition d);
|
||||
class MetricsPeer : public Metrics {
|
||||
public:
|
||||
// Make base class constructors visible
|
||||
using Metrics::FlushMetrics;
|
||||
using Metrics::Metrics;
|
||||
|
||||
bool IsRunning() { return running_; }
|
||||
@@ -72,7 +73,7 @@ using santa::santad::ProcessorToString;
|
||||
- (void)testStartStop {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics =
|
||||
std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, ^(santa::santad::Metrics *m) {
|
||||
std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, ^(santa::santad::Metrics *m) {
|
||||
dispatch_semaphore_signal(self.sema);
|
||||
});
|
||||
|
||||
@@ -107,7 +108,7 @@ using santa::santad::ProcessorToString;
|
||||
|
||||
- (void)testSetInterval {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
});
|
||||
|
||||
@@ -181,7 +182,7 @@ using santa::santad::ProcessorToString;
|
||||
int64_t nanos = 1234;
|
||||
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
@@ -238,11 +239,11 @@ using santa::santad::ProcessorToString;
|
||||
});
|
||||
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics =
|
||||
std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes, mockEventCounts,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes,
|
||||
mockEventCounts, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
EventDisposition::kProcessed, nanos);
|
||||
|
||||
@@ -47,6 +47,29 @@ static void RegisterModeMetric(SNTMetricSet *metricSet) {
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the event log type metric checking the config before reporting the status.
|
||||
*/
|
||||
static void RegisterEventLogType(SNTMetricSet *metricSet) {
|
||||
SNTMetricStringGauge *logType = [metricSet stringGaugeWithName:@"/santa/log_type"
|
||||
fieldNames:@[]
|
||||
helpText:@"Santa's log type"];
|
||||
|
||||
// create a callback that gets the current log type
|
||||
[metricSet registerCallback:^{
|
||||
switch ([[SNTConfigurator configurator] eventLogType]) {
|
||||
case SNTEventLogTypeProtobuf: [logType set:@"protobuf" forFieldValues:@[]]; break;
|
||||
case SNTEventLogTypeSyslog: [logType set:@"syslog" forFieldValues:@[]]; break;
|
||||
case SNTEventLogTypeNull: [logType set:@"null" forFieldValues:@[]]; break;
|
||||
case SNTEventLogTypeFilelog: [logType set:@"file" forFieldValues:@[]]; break;
|
||||
default:
|
||||
// Should never be reached.
|
||||
[logType set:@"unknown" forFieldValues:@[]];
|
||||
break;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register metrics for measuring memory usage.
|
||||
*/
|
||||
@@ -129,6 +152,7 @@ static void RegisterCommonSantaMetrics(SNTMetricSet *metricSet) {
|
||||
value:[SNTSystemInfo osVersion]];
|
||||
|
||||
RegisterModeMetric(metricSet);
|
||||
RegisterEventLogType(metricSet);
|
||||
// TODO(markowsky) Register CSR status
|
||||
// TODO(markowsky) Register system extension status
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
- (void)testRegisteringCoreMetrics {
|
||||
OCMStub([self.mockConfigurator extraMetricLabels]).andReturn(self.extraMetricLabels);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
OCMStub([self.mockConfigurator eventLogType]).andReturn(SNTEventLogTypeProtobuf);
|
||||
|
||||
SNTRegisterCoreMetrics();
|
||||
|
||||
@@ -191,6 +192,18 @@
|
||||
} ],
|
||||
},
|
||||
},
|
||||
@"/santa/log_type" : @{
|
||||
@"description" : @"Santa's log type",
|
||||
@"type" : @6,
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@"created" : fixedDate,
|
||||
@"data" : @"protobuf",
|
||||
@"last_updated" : fixedDate,
|
||||
@"value" : @""
|
||||
} ],
|
||||
},
|
||||
},
|
||||
},
|
||||
@"root_labels" : @{
|
||||
@"host_name" : hostname,
|
||||
|
||||
@@ -77,7 +77,7 @@ double watchdogRAMPeak = 0;
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Kernel ops
|
||||
#pragma mark Cache ops
|
||||
|
||||
- (void)cacheCounts:(void (^)(uint64_t, uint64_t))reply {
|
||||
NSArray<NSNumber *> *counts = self->_authResultCache->CacheCounts();
|
||||
|
||||
@@ -25,6 +25,6 @@
|
||||
- (void)cacheDecision:(SNTCachedDecision *)cd;
|
||||
- (SNTCachedDecision *)cachedDecisionForFile:(const struct stat &)statInfo;
|
||||
- (void)forgetCachedDecisionForFile:(const struct stat &)statInfo;
|
||||
- (void)resetTimestampForCachedDecision:(const struct stat &)statInfo;
|
||||
- (SNTCachedDecision *)resetTimestampForCachedDecision:(const struct stat &)statInfo;
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,17 +17,20 @@
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#import "Source/common/SNTRule.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/common/SantaVnode.h"
|
||||
#include "Source/common/SantaVnodeHash.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
|
||||
@interface SNTDecisionCache ()
|
||||
@property NSMutableDictionary<NSNumber *, SNTCachedDecision *> *detailStore;
|
||||
@property dispatch_queue_t detailStoreQueue;
|
||||
// Cache for sha256 -> date of last timestamp reset.
|
||||
@property NSCache<NSString *, NSDate *> *timestampResetMap;
|
||||
@end
|
||||
|
||||
@implementation SNTDecisionCache
|
||||
@implementation SNTDecisionCache {
|
||||
SantaCache<SantaVnode, SNTCachedDecision *> _decisionCache;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedCache {
|
||||
static SNTDecisionCache *cache;
|
||||
@@ -41,15 +44,6 @@
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
// TODO(mlw): We should protect this structure with a read/write lock
|
||||
// instead of a serial dispatch queue since it's expected that most most
|
||||
// accesses will be lookups, not caching new items.
|
||||
_detailStore = [NSMutableDictionary dictionaryWithCapacity:1000];
|
||||
_detailStoreQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.detail_store",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
|
||||
_timestampResetMap = [[NSCache alloc] init];
|
||||
_timestampResetMap.countLimit = 100;
|
||||
}
|
||||
@@ -57,32 +51,24 @@
|
||||
}
|
||||
|
||||
- (void)cacheDecision:(SNTCachedDecision *)cd {
|
||||
dispatch_sync(self.detailStoreQueue, ^{
|
||||
self.detailStore[@(cd.vnodeId.fileid)] = cd;
|
||||
});
|
||||
self->_decisionCache.set(cd.vnodeId, cd);
|
||||
}
|
||||
|
||||
- (SNTCachedDecision *)cachedDecisionForFile:(const struct stat &)statInfo {
|
||||
__block SNTCachedDecision *cd;
|
||||
dispatch_sync(self.detailStoreQueue, ^{
|
||||
cd = self.detailStore[@(statInfo.st_ino)];
|
||||
});
|
||||
return cd;
|
||||
return self->_decisionCache.get(SantaVnode::VnodeForFile(statInfo));
|
||||
}
|
||||
|
||||
- (void)forgetCachedDecisionForFile:(const struct stat &)statInfo {
|
||||
dispatch_sync(self.detailStoreQueue, ^{
|
||||
[self.detailStore removeObjectForKey:@(statInfo.st_ino)];
|
||||
});
|
||||
self->_decisionCache.remove(SantaVnode::VnodeForFile(statInfo));
|
||||
}
|
||||
|
||||
// Whenever a cached decision resulting from a transitive allowlist rule is used to allow the
|
||||
// execution of a binary, we update the timestamp on the transitive rule in the rules database.
|
||||
// To prevent writing to the database too often, we space out consecutive writes by 3600 seconds.
|
||||
- (void)resetTimestampForCachedDecision:(const struct stat &)statInfo {
|
||||
- (SNTCachedDecision *)resetTimestampForCachedDecision:(const struct stat &)statInfo {
|
||||
SNTCachedDecision *cd = [self cachedDecisionForFile:statInfo];
|
||||
if (!cd || cd.decision != SNTEventStateAllowTransitive || !cd.sha256) {
|
||||
return;
|
||||
return cd;
|
||||
}
|
||||
|
||||
NSDate *lastUpdate = [self.timestampResetMap objectForKey:cd.sha256];
|
||||
@@ -94,6 +80,8 @@
|
||||
[[SNTDatabaseController ruleTable] resetTimestampForRule:rule];
|
||||
[self.timestampResetMap setObject:[NSDate date] forKey:cd.sha256];
|
||||
}
|
||||
|
||||
return cd;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -32,7 +32,7 @@ SNTCachedDecision *MakeCachedDecision(struct stat sb, SNTEventState decision) {
|
||||
cd.decision = decision;
|
||||
cd.sha256 = @"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
||||
cd.vnodeId = {
|
||||
.fsid = 0,
|
||||
.fsid = sb.st_dev,
|
||||
.fileid = sb.st_ino,
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "Source/santad/Santad.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/PrefixTree.h"
|
||||
@@ -310,6 +311,36 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
LOGI(@"StaticRules set has changed, flushing cache.");
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(eventLogType)
|
||||
type:[NSNumber class]
|
||||
callback:^(NSNumber *oldValue, NSNumber *newValue) {
|
||||
NSInteger oldLogType = [oldValue integerValue];
|
||||
NSInteger newLogType = [newValue integerValue];
|
||||
|
||||
if (oldLogType == newLogType) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGW(@"EventLogType config changed (%ld --> %ld). Restarting...", oldLogType,
|
||||
newLogType);
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
logger->Flush();
|
||||
metrics->Export();
|
||||
|
||||
dispatch_semaphore_signal(sema);
|
||||
});
|
||||
|
||||
// Wait for a short amount of time for outstanding data to flush
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
|
||||
|
||||
// Forcefully exit. The daemon will be restarted immediately.
|
||||
exit(EXIT_SUCCESS);
|
||||
}],
|
||||
];
|
||||
|
||||
// Make the compiler happy. The variable is only used to ensure proper lifetime
|
||||
|
||||
@@ -129,6 +129,8 @@ NSDictionary *validMetricsDict = nil;
|
||||
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession alloc])
|
||||
.andReturn(self.mockMOLAuthenticatingURLSession);
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession initWithSessionConfiguration:[OCMArg any]])
|
||||
.andReturn(self.mockMOLAuthenticatingURLSession);
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession session]).andReturn(self.mockSession);
|
||||
|
||||
NSHTTPURLResponse *response =
|
||||
|
||||
@@ -18,94 +18,99 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/santametricservice/Writers/SNTMetricHTTPWriter.h"
|
||||
|
||||
@implementation SNTMetricHTTPWriter {
|
||||
@private
|
||||
MOLAuthenticatingURLSession *_authSession;
|
||||
}
|
||||
@interface SNTMetricHTTPWriter ()
|
||||
@property SNTConfigurator *configurator;
|
||||
@end
|
||||
|
||||
@implementation SNTMetricHTTPWriter
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_authSession = [[MOLAuthenticatingURLSession alloc] init];
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (MOLAuthenticatingURLSession *)createSessionWithHostname:(NSURL *)url
|
||||
Timeout:(NSTimeInterval)timeout {
|
||||
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
config.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12;
|
||||
config.HTTPShouldUsePipelining = YES;
|
||||
config.timeoutIntervalForResource = timeout;
|
||||
|
||||
MOLAuthenticatingURLSession *session =
|
||||
[[MOLAuthenticatingURLSession alloc] initWithSessionConfiguration:config];
|
||||
session.serverHostname = url.host;
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post serialzied metrics to the specified URL one object at a time.
|
||||
**/
|
||||
- (BOOL)write:(NSArray<NSData *> *)metrics toURL:(NSURL *)url error:(NSError **)error {
|
||||
__block NSError *_blockError = nil;
|
||||
NSError *localError;
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
MOLAuthenticatingURLSession *authSession =
|
||||
[self createSessionWithHostname:url Timeout:self.configurator.metricExportTimeout];
|
||||
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
||||
request.HTTPMethod = @"POST";
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||
|
||||
_authSession.serverHostname = url.host;
|
||||
NSURLSession *_session = _authSession.session;
|
||||
for (NSData *metric in metrics) {
|
||||
NSError *taskError;
|
||||
NSURLResponse *taskResponse;
|
||||
request.HTTPBody = (NSData *)metric;
|
||||
|
||||
dispatch_group_t requests = dispatch_group_create();
|
||||
// Note: In order to help ease mock writing in tests, the `task` variable is
|
||||
// sequestered into this anonymous scope to help ensure future edits outside
|
||||
// of this scope don'taccess the `task` variable which could mess up how the
|
||||
// tests are written.
|
||||
{
|
||||
NSURLSessionDataTask *task = [authSession.session
|
||||
dataTaskWithRequest:request
|
||||
completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response,
|
||||
NSError *_Nullable err) {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
[metrics enumerateObjectsUsingBlock:^(id value, NSUInteger index, BOOL *stop) {
|
||||
dispatch_group_enter(requests);
|
||||
[task resume];
|
||||
|
||||
request.HTTPBody = (NSData *)value;
|
||||
NSURLSessionDataTask *task = [_session
|
||||
dataTaskWithRequest:request
|
||||
completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response,
|
||||
NSError *_Nullable err) {
|
||||
if (err != nil) {
|
||||
_blockError = err;
|
||||
*stop = YES;
|
||||
} else if (response == nil) {
|
||||
*stop = YES;
|
||||
} else if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
||||
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
||||
|
||||
// Check HTTP error codes and create errors for any non-200.
|
||||
if (httpResponse && httpResponse.statusCode != 200) {
|
||||
_blockError = [[NSError alloc]
|
||||
initWithDomain:@"com.google.santa.metricservice.writers.http"
|
||||
code:httpResponse.statusCode
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey :
|
||||
[NSString stringWithFormat:@"received http status code %ld from %@",
|
||||
httpResponse.statusCode, url]
|
||||
}];
|
||||
*stop = YES;
|
||||
}
|
||||
}
|
||||
dispatch_group_leave(requests);
|
||||
}];
|
||||
|
||||
[task resume];
|
||||
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
int64_t timeout = (int64_t)config.metricExportTimeout;
|
||||
|
||||
// Wait up to timeout seconds for the request to complete.
|
||||
if (dispatch_group_wait(requests, dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC))) !=
|
||||
0) {
|
||||
[task cancel];
|
||||
NSString *errMsg =
|
||||
[NSString stringWithFormat:@"HTTP request to %@ timed out after %lu seconds", url,
|
||||
(unsigned long)timeout];
|
||||
|
||||
_blockError = [[NSError alloc] initWithDomain:@"com.google.santa.metricservice.writers.http"
|
||||
code:ETIMEDOUT
|
||||
userInfo:@{NSLocalizedDescriptionKey : errMsg}];
|
||||
}
|
||||
}];
|
||||
|
||||
if (_blockError != nil) {
|
||||
// If the caller hasn't passed us an error then we ignore it.
|
||||
if (error != nil) {
|
||||
*error = [_blockError copy];
|
||||
// Note: Extracting property values once here to make test mock writing simpler
|
||||
taskError = task.error;
|
||||
taskResponse = task.response;
|
||||
}
|
||||
|
||||
return NO;
|
||||
// Note: localError will only store the last error that occured while
|
||||
// sending items from the array of metrics.
|
||||
if (taskError) {
|
||||
localError = taskError;
|
||||
} else if ([taskResponse isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
NSInteger statusCode = ((NSHTTPURLResponse *)taskResponse).statusCode;
|
||||
if (statusCode != 200) {
|
||||
localError = [[NSError alloc]
|
||||
initWithDomain:@"com.google.santa.metricservice.writers.http"
|
||||
code:statusCode
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : [NSString
|
||||
stringWithFormat:@"received http status code %ld from %@", statusCode, url]
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
if (error != nil) {
|
||||
*error = localError;
|
||||
}
|
||||
|
||||
// Success is determined by whether or not any failures occured while sending
|
||||
// any of the metrics.
|
||||
return localError == nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession alloc])
|
||||
.andReturn(self.mockMOLAuthenticatingURLSession);
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession initWithSessionConfiguration:[OCMArg any]])
|
||||
.andReturn(self.mockMOLAuthenticatingURLSession);
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession session]).andReturn(self.mockSession);
|
||||
|
||||
self.httpWriter = [[SNTMetricHTTPWriter alloc] init];
|
||||
@@ -46,6 +48,15 @@
|
||||
|
||||
void (^callCompletionHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
|
||||
NSDictionary *responseValue = self.mockResponses[0];
|
||||
|
||||
if (responseValue[@"error"] != nil) {
|
||||
OCMExpect([(NSURLSessionDataTask *)self.mockSessionDataTask error])
|
||||
.andReturn(responseValue[@"error"]);
|
||||
} else if (((NSHTTPURLResponse *)responseValue[@"response"]).statusCode != 200) {
|
||||
OCMExpect([(NSURLSessionDataTask *)self.mockSessionDataTask response])
|
||||
.andReturn(responseValue[@"response"]);
|
||||
}
|
||||
|
||||
if (responseValue != nil && completionHandler != nil) {
|
||||
completionHandler(responseValue[@"data"], responseValue[@"response"],
|
||||
responseValue[@"error"]);
|
||||
|
||||
@@ -30,52 +30,49 @@
|
||||
- (BOOL)sync {
|
||||
[self performRequest:[self requestWithDictionary:nil]];
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
void (^replyBlock)(void) = ^{
|
||||
dispatch_group_leave(group);
|
||||
};
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
|
||||
// Set client mode if it changed
|
||||
if (self.syncState.clientMode) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setClientMode:self.syncState.clientMode reply:replyBlock];
|
||||
[rop setClientMode:self.syncState.clientMode
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
|
||||
// Remove clean sync flag if we did a clean sync
|
||||
if (self.syncState.cleanSync) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setSyncCleanRequired:NO reply:replyBlock];
|
||||
[rop setSyncCleanRequired:NO
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
|
||||
// Update allowlist/blocklist regexes
|
||||
if (self.syncState.allowlistRegex) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setAllowedPathRegex:self.syncState.allowlistRegex
|
||||
reply:replyBlock];
|
||||
[rop setAllowedPathRegex:self.syncState.allowlistRegex
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
if (self.syncState.blocklistRegex) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setBlockedPathRegex:self.syncState.blocklistRegex
|
||||
reply:replyBlock];
|
||||
[rop setBlockedPathRegex:self.syncState.blocklistRegex
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
|
||||
if (self.syncState.blockUSBMount != nil) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setBlockUSBMount:[self.syncState.blockUSBMount boolValue]
|
||||
reply:replyBlock];
|
||||
[rop setBlockUSBMount:[self.syncState.blockUSBMount boolValue]
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
if (self.syncState.remountUSBMode) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setRemountUSBMode:self.syncState.remountUSBMode
|
||||
reply:replyBlock];
|
||||
[rop setRemountUSBMode:self.syncState.remountUSBMode
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
|
||||
// Update last sync success
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setFullSyncLastSuccess:[NSDate date] reply:replyBlock];
|
||||
|
||||
// Wait for dispatch group
|
||||
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
|
||||
[rop setFullSyncLastSuccess:[NSDate date]
|
||||
reply:^{
|
||||
}];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@@ -44,42 +44,32 @@
|
||||
requestDict[kFCMToken] = self.syncState.pushNotificationsToken;
|
||||
}
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler, int64_t transitive,
|
||||
int64_t teamID) {
|
||||
requestDict[kBinaryRuleCount] = @(binary);
|
||||
requestDict[kCertificateRuleCount] = @(certificate);
|
||||
requestDict[kCompilerRuleCount] = @(compiler);
|
||||
requestDict[kTransitiveRuleCount] = @(transitive);
|
||||
requestDict[kTeamIDRuleCount] = @(teamID);
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] clientMode:^(SNTClientMode cm) {
|
||||
// dispatch_group_t group = dispatch_group_create();
|
||||
// dispatch_group_enter(group);
|
||||
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID) {
|
||||
requestDict[kBinaryRuleCount] = @(binary);
|
||||
requestDict[kCertificateRuleCount] = @(certificate);
|
||||
requestDict[kCompilerRuleCount] = @(compiler);
|
||||
requestDict[kTransitiveRuleCount] = @(transitive);
|
||||
requestDict[kTeamIDRuleCount] = @(teamID);
|
||||
}];
|
||||
|
||||
[rop clientMode:^(SNTClientMode cm) {
|
||||
switch (cm) {
|
||||
case SNTClientModeMonitor: requestDict[kClientMode] = kClientModeMonitor; break;
|
||||
case SNTClientModeLockdown: requestDict[kClientMode] = kClientModeLockdown; break;
|
||||
default: break;
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
__block BOOL syncClean = NO;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] syncCleanRequired:^(BOOL clean) {
|
||||
[rop syncCleanRequired:^(BOOL clean) {
|
||||
syncClean = clean;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Stop the sync if we are unable to communicate with daemon.
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC))) {
|
||||
SLOGE(@"Unable to communicate with daemon.");
|
||||
return NO;
|
||||
}
|
||||
|
||||
// If user requested it or we've never had a successful sync, try from a clean slate.
|
||||
if (syncClean) {
|
||||
SLOGD(@"Clean sync requested by user");
|
||||
@@ -91,38 +81,29 @@
|
||||
|
||||
if (!resp) return NO;
|
||||
|
||||
dispatch_group_enter(group);
|
||||
NSNumber *enableBundles = resp[kEnableBundles];
|
||||
if (!enableBundles) enableBundles = resp[kEnableBundlesDeprecated];
|
||||
[[self.daemonConn remoteObjectProxy] setEnableBundles:[enableBundles boolValue]
|
||||
reply:^{
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
[rop setEnableBundles:[enableBundles boolValue]
|
||||
reply:^{
|
||||
}];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
NSNumber *enableTransitiveRules = resp[kEnableTransitiveRules];
|
||||
if (!enableTransitiveRules) enableTransitiveRules = resp[kEnableTransitiveRulesDeprecated];
|
||||
if (!enableTransitiveRules) enableTransitiveRules = resp[kEnableTransitiveRulesSuperDeprecated];
|
||||
BOOL enabled = [enableTransitiveRules boolValue];
|
||||
[[self.daemonConn remoteObjectProxy] setEnableTransitiveRules:enabled
|
||||
reply:^{
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
[rop setEnableTransitiveRules:enabled
|
||||
reply:^{
|
||||
}];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
NSNumber *enableAllEventUpload = resp[kEnableAllEventUpload];
|
||||
[[self.daemonConn remoteObjectProxy] setEnableAllEventUpload:[enableAllEventUpload boolValue]
|
||||
reply:^{
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
[rop setEnableAllEventUpload:[enableAllEventUpload boolValue]
|
||||
reply:^{
|
||||
}];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
NSNumber *disableUnknownEventUpload = resp[kDisableUnknownEventUpload];
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
setDisableUnknownEventUpload:[disableUnknownEventUpload boolValue]
|
||||
reply:^{
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
[rop setDisableUnknownEventUpload:[disableUnknownEventUpload boolValue]
|
||||
reply:^{
|
||||
}];
|
||||
|
||||
self.syncState.eventBatchSize = [resp[kBatchSize] unsignedIntegerValue] ?: kDefaultEventBatchSize;
|
||||
|
||||
@@ -170,7 +151,6 @@
|
||||
self.syncState.cleanSync = YES;
|
||||
}
|
||||
|
||||
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC));
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
self.syncState.daemonConn = OCMClassMock([MOLXPCConnection class]);
|
||||
self.daemonConnRop = OCMProtocolMock(@protocol(SNTDaemonControlXPC));
|
||||
OCMStub([self.syncState.daemonConn remoteObjectProxy]).andReturn(self.daemonConnRop);
|
||||
OCMStub([self.syncState.daemonConn synchronousRemoteObjectProxy]).andReturn(self.daemonConnRop);
|
||||
|
||||
self.syncState.session = OCMClassMock([NSURLSession class]);
|
||||
|
||||
|
||||
38
WORKSPACE
38
WORKSPACE
@@ -7,6 +7,15 @@ load(
|
||||
)
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
# We don't directly use rules_python but several dependencies do and they disagree
|
||||
# about which version to use, so we force the latest.
|
||||
http_archive(
|
||||
name = "rules_python",
|
||||
sha256 = "48a838a6e1983e4884b26812b2c748a35ad284fd339eb8e2a6f3adf95307fbcd",
|
||||
strip_prefix = "rules_python-0.16.2",
|
||||
url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.16.2.tar.gz",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_apple",
|
||||
sha256 = "f003875c248544009c8e8ae03906bbdacb970bc3e5931b40cd76cadeded99632", # 1.1.0
|
||||
@@ -21,6 +30,26 @@ load("@build_bazel_apple_support//lib:repositories.bzl", "apple_support_dependen
|
||||
|
||||
apple_support_dependencies()
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_swift",
|
||||
sha256 = "84e2cc1c9e3593ae2c0aa4c773bceeb63c2d04c02a74a6e30c1961684d235593",
|
||||
url = "https://github.com/bazelbuild/rules_swift/releases/download/1.5.1/rules_swift.1.5.1.tar.gz",
|
||||
)
|
||||
|
||||
load(
|
||||
"@build_bazel_rules_swift//swift:repositories.bzl",
|
||||
"swift_rules_dependencies",
|
||||
)
|
||||
|
||||
swift_rules_dependencies()
|
||||
|
||||
load(
|
||||
"@build_bazel_rules_swift//swift:extras.bzl",
|
||||
"swift_rules_extra_dependencies",
|
||||
)
|
||||
|
||||
swift_rules_extra_dependencies()
|
||||
|
||||
# Hedron Bazel Compile Commands Extractor
|
||||
# Allows integrating with clangd
|
||||
# https://github.com/hedronvision/bazel-compile-commands-extractor
|
||||
@@ -181,10 +210,19 @@ gazelle_dependencies()
|
||||
|
||||
# Fuzzing
|
||||
|
||||
# rules_fuzzing requires an older python for now
|
||||
http_archive(
|
||||
name = "rules_python_fuzz",
|
||||
sha256 = "c03246c11efd49266e8e41e12931090b613e12a59e6f55ba2efd29a7cb8b4258",
|
||||
strip_prefix = "rules_python-0.11.0",
|
||||
url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.11.0.tar.gz",
|
||||
)
|
||||
|
||||
git_repository(
|
||||
name = "rules_fuzzing",
|
||||
commit = "b193df79b10dbfb4c623bda23e825e835f12bada", # Commit post PR 213 which fixes macOS
|
||||
remote = "https://github.com/bazelbuild/rules_fuzzing",
|
||||
repo_mapping = {"@rules_python": "@rules_python_fuzz"},
|
||||
shallow_since = "1668184479 -0500",
|
||||
)
|
||||
|
||||
|
||||
14
docs/_sass/custom/custom.scss
Normal file
14
docs/_sass/custom/custom.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
// Support wider pages
|
||||
@media (min-width: 50rem) {
|
||||
.main {
|
||||
max-width: none;
|
||||
}
|
||||
.side-bar {
|
||||
max-width: 380px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1298px) {
|
||||
.main {
|
||||
margin-left: 380px;
|
||||
}
|
||||
}
|
||||
170
docs/deployment/file-access-auth.md
Normal file
170
docs/deployment/file-access-auth.md
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
title: File Access Authorization
|
||||
parent: Deployment
|
||||
nav_order: 4
|
||||
---
|
||||
|
||||
# (BETA) File Access Authorization
|
||||
|
||||
> **IMPORTANT:** This feature is in beta. Configuration and log formats are subject to change.
|
||||
|
||||
> **IMPORTANT:** This feature is only supported on macOS 13 and above.
|
||||
|
||||
File Access Authorization allows admins to configure Santa to monitor filesystem paths for potentially unwanted access and optionally deny the operation.
|
||||
|
||||
## Enabling the Feature
|
||||
|
||||
To enable this feature, the `FileAccessPolicyPlist` key in the main [Santa configuration](configuration.md) must contain the path to a configuration file. See the format specified in the [Configuration](#configuration) section below. The Santa configuration can also contain the `FileAccessPolicyUpdateIntervalSec` that dictates how often the File Access Authorization configuration is re-applied (see the details on [Path Globs](#path-globs) for more information on what happens during updates).
|
||||
|
||||
## Configuration
|
||||
|
||||
| Key | Parent | Type | Required | Description |
|
||||
| ------------------- | ------------ | ---------- | -------- | ----------- |
|
||||
| `Version` | `<Root>` | String | Yes | Version of the configuration. Will be reported in events. |
|
||||
| `WatchItems` | `<Root>` | Dictionary | No | The set of configuration items that will be monitored by Santa. |
|
||||
| `<Name>` | `WatchItems` | Dictionary | No | A unique name that identifies a single watch item rule. This value will be reported in events. The name must be a legal C identifier (i.e., must conform to the regex `[A-Za-z_][A-Za-z0-9_]*`). |
|
||||
| `Paths` | `<Name>` | Array | Yes | A list of either String or Dictionary types that contain path globs to monitor. String type entires will have default values applied for the attributes that can be manually set with the Dictionary type. |
|
||||
| `Path` | `Paths` | String | Yes | The path glob to monitor. |
|
||||
| `IsPrefix` | `Paths` | Boolean | No | Whether or not the path glob represents a prefix path. (Default = `false`) |
|
||||
| `Options` | `<Name>` | Dictionary | No | Customizes the actions for a given rule. |
|
||||
| `AllowReadAccess` | `Options` | Boolean | No | If true, indicates the rule will **not** be applied to actions that are read-only access (e.g., opening a watched path for reading, or cloning a watched path). If false, the rule will apply both to read-only access and access that could modify the watched path. (Default = `false`) |
|
||||
| `AuditOnly` | `Options` | Boolean | No | If true, operations violating the rule will only be logged. If false, operations violating the rule will be denied and logged. (Default = `true`) |
|
||||
| `Processes` | `<Name>` | Array | No | A list of dictionaries defining processes that are allowed to access paths matching the globs defined with the `Paths` key. For a process performing the operation to be considered a match, it must match all defined attributes of at least one entry in the list. |
|
||||
| `BinaryPath` | `Processes` | String | No | A path literal that an instigating process must be executed from. |
|
||||
| `TeamID` | `Processes` | String | No | Team ID of the instigating process. |
|
||||
| `CertificateSha256` | `Processes` | String | No | SHA256 of the leaf certificate of the instigating process. |
|
||||
| `CDHash` | `Processes` | String | No | CDHash of the instigating process. |
|
||||
|
||||
### Example Configuration
|
||||
|
||||
This is an example configuration conforming to the specification outlined above:
|
||||
|
||||
```xml
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Version</key>
|
||||
<string>v0.1-experimental</string>
|
||||
<key>WatchItems</key>
|
||||
<dict>
|
||||
<key>UserFoo</key>
|
||||
<dict>
|
||||
<key>Paths</key>
|
||||
<array>
|
||||
<string>/Users/*/foo</string>
|
||||
<dict>
|
||||
<key>Path</key>
|
||||
<string>/Users/*/tmp/foo</string>
|
||||
<key>IsPrefix</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</array>
|
||||
<key>Options</key>
|
||||
<dict>
|
||||
<key>AllowReadAccess</key>
|
||||
<false/>
|
||||
<key>AuditOnly</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>Processes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>/usr/local/bin/my_foo_writer</string>
|
||||
<key>TeamID</key>
|
||||
<string>ABCDEF1234</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
### Rule Matching
|
||||
|
||||
When an operation occurs on a path that matches multiple configured path globs, the rule containing the "most specific" matching path glob is applied (i.e., the longest matching path).
|
||||
|
||||
For example, consider the configured rules and paths:
|
||||
```
|
||||
RULE_1: /tmp/foo [IsPrefix=true]
|
||||
RULE_2: /tmp/foo.txt [IsPrefix=false]
|
||||
RULE_3: /tmp [IsPrefix=true]
|
||||
```
|
||||
The following table demonstrates which rule will be applied for operations on a given path:
|
||||
|
||||
| Operation Path | Rule Applied | Reason |
|
||||
| ---------------- | ------------ | ------ |
|
||||
| /tmp/foo | `RULE_1` | Matches prefix, more specific than `RULE_3` |
|
||||
| /tmp/foo/bar | `RULE_1` | Matches prefix, more specific than `RULE_3` |
|
||||
| /tmp/bar | `RULE_3` | Matches prefix |
|
||||
| /tmp/foo.txt | `RULE_2` | Matches literal, more specific than `RULE_1` |
|
||||
| /tmp/foo.txt.tmp | `RULE_1` | Matches prefix, more specific than `RULE_3`, literal match doesn't apply |
|
||||
| /foo | N/A | No rules match operations on this path |
|
||||
|
||||
> **IMPORTANT:** If a configuration contains multiple rules with duplicate configured paths, only one rule will be applied to the path. It is undefined which configured rule will be used. Administrators should take care not to define configurations that may have duplicate paths.
|
||||
|
||||
### Path Globs
|
||||
|
||||
Configured path globs represent a point in time. That is, path globs are expanded when a configuration is applied to generate the set of monitored paths. This is not a "live" representation of the filesystem. For instance, if a new file or directory is added that would match a glob after the configuration is applied, it is not immediately monitored.
|
||||
|
||||
Within the main Santa configuration, the `FileAccessPolicyUpdateIntervalSec` key controls how often any changes to the configuration are applied as well as re-evaluating configured path globs to match the current state of the filesystem.
|
||||
|
||||
### Prefix and Glob Path Evaluation
|
||||
|
||||
Combining path globs and the `IsPrefix` key in a configuration gives greater control over the paths that rules should match. Consider the configured path globs:
|
||||
```
|
||||
PG_1: /tmp/* [IsPrefix = false]
|
||||
PG_2: /tmp/* [IsPrefix = true]
|
||||
PG_3: /tmp/ [IsPrefix = true]
|
||||
PG_4: /tmp/file1.txt [IsPrefix = false]
|
||||
```
|
||||
|
||||
And a filesystem that contains:
|
||||
```
|
||||
/
|
||||
tmp/
|
||||
file1.txt
|
||||
file2.txt
|
||||
dir1/
|
||||
d1_f1.txt
|
||||
d1_f2.txt
|
||||
```
|
||||
|
||||
Now, assume the configuration is applied, and moments later a new file (`/tmp/file3_new.txt`) and a new directory (`/tmp/dir2_new`) are both created:
|
||||
* `PG_1` will match against the two original files within `/tmp` and the one directory `dir1` itself (but not nested contents).
|
||||
* `PG_2` will match against the two original files within `/tmp` and the one directory `dir1` (as well as nested contents).
|
||||
* `PG_3` will match against all original and newly created files and directories within `/tmp` (as well as nested contents).
|
||||
* `PG_4` will only match `/tmp/file1.txt`.
|
||||
|
||||
### Case Sensitivity
|
||||
|
||||
All configured paths are case sensitive (i.e., paths specified in both the `Paths` and `BinaryPath` configuration keys). The case must match the case of the path as stored on the filesystem.
|
||||
|
||||
### Hard Links
|
||||
|
||||
Due to platform limitations, it is not feasible for Santa to know all links for a given path. To help mitigate bypasses to this feature, Santa will not allow hard links to be created for monitored paths. If hard links previously existed to monitored paths, Santa cannot guarantee that access to watched resources via these other links will be monitored.
|
||||
|
||||
### Symbolic Links
|
||||
|
||||
Configured path globs must refer to resolved paths only. It is not supported to monitor access on symbolic links. For example, consider the configured path globs:
|
||||
|
||||
```
|
||||
PG_1: /var/audit/ [IsPrefix = true]
|
||||
PG_2: /private/var/audit/ [IsPrefix = true]
|
||||
```
|
||||
|
||||
`PG_1` will not match any operations because `/var` is a symbolic link. `PG_2` however is properly configured and will match on access to items in the configured directory.
|
||||
|
||||
## Logging
|
||||
|
||||
When an operation matches a defined rule, and the instigating process did not match any of the defined exceptions in the `Processes` key, the operation will be logged. Both string and protobuf logging are supported.
|
||||
|
||||
When the `EventLogType` configuration key is set to `syslog` or `file`, an example log message will look like:
|
||||
```
|
||||
action=FILE_ACCESS|policy_version=v0.1-experimental|policy_name=UserFoo|path=/Users/local/tmp/foo/text.txt|access_type=OPEN|decision=AUDIT_ONLY|pid=12|ppid=56|process=cat|processpath=/bin/cat|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id
|
||||
```
|
||||
|
||||
When the `EventLogType` configuration key is set to `protobuf`, a log is emitted to match the `FileAccess` message in the [santa.proto](https://github.com/google/santa/blob/main/Source/common/santa.proto) schema.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Sync Servers
|
||||
parent: Deployment
|
||||
nav_order: 4
|
||||
nav_order: 5
|
||||
---
|
||||
|
||||
# Sync Servers
|
||||
@@ -12,7 +12,7 @@ There are several open-source servers you can sync with:
|
||||
|
||||
* [Moroz](https://github.com/groob/moroz): A simple golang server that serves hard-coded rules from configuration files.
|
||||
* [Rudolph](https://github.com/airbnb/rudolph): An AWS-based serverless sync service primarily built on API GW, DynamoDB, and Lambda components to reduce operational burden. Rudolph is designed to be fast, easy-to-use, and cost-efficient.
|
||||
* [Zentral](https://github.com/zentralopensource/zentral/wiki): A centralized service that pulls data from multiple sources and deploys configurations to multiple services.
|
||||
* [Zentral](https://github.com/zentralopensource/zentral): A centralized sync service that pulls data from multiple sources and dynamically manages endpoint agents. Zentral is designed to facilitate modern GitOps workflows by unifying a full suite of application management, binary control and reporting with Apple MDM capability. The Santa rules in Zentral can be managed via [Terraform](https://registry.terraform.io/providers/zentralopensource/zentral/latest/docs/resources/santa_rule).
|
||||
* [Zercurity](https://github.com/zercurity/zercurity): A dockerized service for managing and monitoring applications across a large fleet using Santa + Osquery.
|
||||
|
||||
Alternatively, `santactl` can configure rules locally without a sync server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Troubleshooting
|
||||
parent: Deployment
|
||||
nav_order: 5
|
||||
nav_order: 6
|
||||
---
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
@@ -46,7 +46,7 @@ sequenceDiagram
|
||||
server -->> client: eventupload response (200)
|
||||
end
|
||||
loop until all rules are downloaded
|
||||
client ->> server: GET /ruledownload/<machine_id>
|
||||
client ->> server: POST /ruledownload/<machine_id>
|
||||
server --> client: ruledownload response (200)
|
||||
end
|
||||
client ->> server: POST /postflight/<machine_id> request
|
||||
|
||||
@@ -34,6 +34,7 @@ The following pages give an overview of how Santa accomplishes authorization at
|
||||
|
||||
* [Getting Started](deployment/getting-started.md): A quick guide to setting up your deployment.
|
||||
* [Configuration](deployment/configuration.md): The local and sync server configuration options, along with example needed mobileconfig files.
|
||||
* (BETA) [File Access Authorization](deployment/file-access-auth.md): Guide to enabling the feature and details about its configuration and operation.
|
||||
* [Sync Servers](deployment/sync-servers.md): A list of open-source sync servers.
|
||||
* [Troubleshooting](deployment/troubleshooting.md): How to troubleshoot issues with your Santa deployment.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user