Compare commits

..

32 Commits

Author SHA1 Message Date
Matt W
61558048c0 Add basic metrics to report when the FAM client is enabled (#1043) 2023-02-17 11:57:18 -05:00
Matt W
cf0e3fd3db Add support for platform binary to process exceptions (#1041)
* Add support for platform bianry to process exceptions

* Fun with bool types
2023-02-17 11:30:46 -05:00
Matt W
15519c6de8 Clear ES cache when watch items change (#1042) 2023-02-17 11:04:08 -05:00
Pete Markowsky
a415679980 Fix sync protocol diagram. (#1037) 2023-02-08 16:13:08 -05:00
Nick Gregory
27ae60e265 Small test fixes to make sanitizers happy (#1030)
* Small test fixes to make sanitizers happy

* lint

* missing authclient

* new MockEndpointSecurityAPI per subtest
2023-02-06 20:16:22 +00:00
Matt W
29a50f072c Report log type in santactl status (#1036)
* Report log type in santactl status

* Remove unnecessary fallback case
2023-02-06 14:59:42 -05:00
Matt W
a97e82e316 Replace SNTDecisionCache dictionary with SantaCache (#1034)
* Replace SNTDecisionCache dictionary with SantaCache

* PR feedback. Fix tests.
2023-02-03 15:58:53 -05:00
Russell Hancox
532120ac02 Configurator: Return an unsafe_unretained pointer to avoid needless retain/release (#1035) 2023-02-03 15:55:15 -05:00
Russell Hancox
ec934854fc santactl & syncservice: Use synchronousRemoteObjectProxy where it makes sense (#1033) 2023-02-03 14:31:37 -05:00
Matt W
ad0e2abdac Restart daemon on log type change (#1031)
* WIP register for event log type changes. Flush metrics.

* Add Flush to writer interface. Flush logger on log type change.

* Standardize non-thread-safe method names
2023-02-03 11:04:57 -05:00
Matt W
dc11ea6534 Rework timeout handling in metrics HTTP writer (#1029)
* Change HTTP writer to use session config timeouts

* Remove unnecessary block variable

* Fix tests

* Revert serializer changes for now

* Remove setting timeoutIntervalForRequest
2023-02-02 10:58:28 -05:00
Matt W
3acf3c1d00 Use cached sizes when serializing (#1028) 2023-01-30 16:08:38 -05:00
Matt W
41bc3d2542 Perf: Translocate cache, reserve proto repeated fields (#1027)
* Translocate cache, reserve proto repeated fields

* Remove copy/paste
2023-01-30 12:18:32 -05:00
Pete Markowsky
45a5d4e800 Fix: Rewrite the SNTMetricHTTPWriter to avoid potential stack corruption (#1019)
* Updated the SNTMetricHTTPWriter to use a for loop to prevent crashes caused by writing to stop.

* Make requests serial again.

* Fix the typo,  I just pushed.

* Ensure we only lookup the timeout value once.

* Make SNTConfigurator assignment only happen once.
2023-01-30 11:53:26 -05:00
Matt W
82bd981f31 Fix team ID and signing ID checks (#1026)
* Fix policy checks with missing team/signing ids

* Update docs to clarify how symbolic links are handled
2023-01-30 09:14:27 -05:00
Russell Hancox
6480d9c99b docs: fix width of sidebar on larger windows (#1025) 2023-01-27 15:38:46 -05:00
Henry S
7e963080b3 add updated description (#1023)
Zentral has gained many more Santa-specific workflows since adding to this section in 2017. The updated description takes this into account.
2023-01-27 15:38:14 -05:00
Matt W
e58cd7d125 Remove Default column (#1024) 2023-01-27 15:28:31 -05:00
Russell Hancox
db597e413b docs: Support wider pages, fix syntax highlighting of plist (#1022) 2023-01-27 15:18:45 -05:00
Matt W
78f46896d5 Try with more vertical space (#1021) 2023-01-27 14:37:02 -05:00
Matt W
cc0742dbfb Fsmon docs table width (#1020)
* markdown spaces lol

* markdown vertical spaces lol

* more spaces why not
2023-01-27 14:32:58 -05:00
Matt W
9c2f76af72 Initial docs for file access auth feature (#1017)
* Initial docs for file access auth feature

* Apply suggestions from code review

Co-authored-by: Kathryn Hancox <44557882+kathancox@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Kathryn Hancox <44557882+kathancox@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Kathryn Hancox <44557882+kathancox@users.noreply.github.com>

* Updates based on PR feedback

---------

Co-authored-by: Kathryn Hancox <44557882+kathancox@users.noreply.github.com>
2023-01-27 14:08:34 -05:00
Matt W
a3ed5ccb40 Log type metrics (#1018)
* Add event log type to metrics

* lint

* PR Feedback
2023-01-27 10:22:09 -05:00
Nick Gregory
b4149816c7 Add new continuous test run with various sanitizers (#1016)
* continuous tests with sanitizer matrix

* dyld insert lib

* remove msan config and upload logs
2023-01-26 16:00:47 -05:00
Matt W
2313d6338d Remove extra expectation in test (#1015) 2023-01-26 11:42:14 -05:00
Russell Hancox
414fbff721 Project: Fix module maps for swift libraries and their dependencies (#1014) 2023-01-26 09:15:30 -05:00
Matt W
5a2e42e9b4 Reduce calls into configurator (#1013) 2023-01-25 16:51:13 -05:00
Matt W
f8d1b2e880 Reduce proto warning severity (#1012) 2023-01-25 14:37:00 -05:00
Matt W
5f4d2a92fc Ensure watch item names conform to naming requirements (#1011)
* Ensure watch item names conform to naming requirements

* Only compile regex once
2023-01-25 13:27:27 -05:00
Russell Hancox
4ccffdca01 GUI: Migrate DeviceMessageWindow to SwiftUI (#1010) 2023-01-25 12:16:31 -05:00
Nick Gregory
e60bbe1b55 shadow rules_python for fuzzing (#1009) 2023-01-23 11:11:48 -05:00
Russell Hancox
eee2149439 GUI: Re-write AboutWindow view in SwiftUI (#1007) 2023-01-20 13:43:50 -05:00
82 changed files with 1278 additions and 837 deletions

View File

@@ -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
View 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*

View File

@@ -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(

View File

@@ -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.

View File

@@ -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";
}

View File

@@ -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;

View File

@@ -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";

View File

@@ -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",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View 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)
}
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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 {

View 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)
}
}

View File

@@ -4,7 +4,7 @@
- (void)windowDidCloseSilenceHash:(NSString *)hash;
@end
@interface SNTMessageWindowController : NSWindowController
@interface SNTMessageWindowController : NSWindowController <NSWindowDelegate>
- (IBAction)showWindow:(id)sender;
- (IBAction)closeWindow:(id)sender;

View File

@@ -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;

View File

@@ -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");

View File

@@ -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"]];

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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

View File

@@ -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", @{

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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();

View File

@@ -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];
}

View File

@@ -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);

View File

@@ -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:

View File

@@ -107,4 +107,8 @@ void Logger::LogFileAccess(
enriched_process, target, decision));
}
void Logger::Flush() {
writer_->Flush();
}
} // namespace santa::santad::logs::endpoint_security

View File

@@ -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(

View File

@@ -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());

View File

@@ -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);
}

View File

@@ -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(

View File

@@ -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 {};
}

View File

@@ -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);

View File

@@ -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(

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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(

View File

@@ -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);

View File

@@ -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];

View File

@@ -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_;

View File

@@ -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_);

View File

@@ -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());

View File

@@ -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

View File

@@ -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

View File

@@ -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_;

View File

@@ -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.

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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_) {

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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();

View File

@@ -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

View File

@@ -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

View File

@@ -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,
};

View File

@@ -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

View File

@@ -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 =

View File

@@ -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

View File

@@ -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"]);

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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]);

View File

@@ -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",
)

View 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;
}
}

View 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.

View File

@@ -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.

View File

@@ -1,7 +1,7 @@
---
title: Troubleshooting
parent: Deployment
nav_order: 5
nav_order: 6
---
# Troubleshooting

View File

@@ -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

View File

@@ -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.