mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08697d9daf | ||
|
|
8959871988 | ||
|
|
bb43a04992 | ||
|
|
5f93dc7991 | ||
|
|
9be8eb223c | ||
|
|
e8b6c47e0f | ||
|
|
697d442afb | ||
|
|
5dbd261b5a | ||
|
|
9bc94ca658 | ||
|
|
4404b5f849 | ||
|
|
6a4b73b8a9 | ||
|
|
b6146224b3 | ||
|
|
e3593c1b0c | ||
|
|
90a2f10da6 | ||
|
|
60bab1c004 | ||
|
|
0898940d0b | ||
|
|
38b65b0ca4 | ||
|
|
d36ce5eefc | ||
|
|
ff99ab9cfe | ||
|
|
64995367c3 | ||
|
|
c67f0ffc11 | ||
|
|
d5403ae112 | ||
|
|
d21d64cbfe | ||
|
|
347ee3c4f5 | ||
|
|
77ed1cca29 | ||
|
|
cfac7dbb37 | ||
|
|
f27d72f3f9 | ||
|
|
3cd93b287e | ||
|
|
5e5605881b | ||
|
|
a9b48610df | ||
|
|
3cca09a48c | ||
|
|
3134448eac | ||
|
|
663bdf945b | ||
|
|
e94d1175e7 | ||
|
|
e20b761965 | ||
|
|
90c64812d0 | ||
|
|
08d368fc49 | ||
|
|
39385f0bff | ||
|
|
8bc3418ce1 | ||
|
|
a145700398 | ||
|
|
409535e617 | ||
|
|
f625016efe | ||
|
|
f4c94ab1d7 | ||
|
|
8234706dd3 | ||
|
|
1a31dc870f | ||
|
|
a1712858c5 | ||
|
|
0059e768b9 | ||
|
|
4fe1550bd2 | ||
|
|
0c182c8a7f | ||
|
|
bcdf746def | ||
|
|
bc13ac3a98 | ||
|
|
a894e018cd | ||
|
|
cbecfd444d | ||
|
|
357e5ef963 | ||
|
|
60594c9f03 | ||
|
|
44b5bae8da | ||
|
|
2e856196c5 | ||
|
|
8672187c02 |
@@ -30,7 +30,6 @@ PACKAGE_VERSION:=$(shell curl -fs https://api.github.com/repos/google/santa/rele
|
||||
# |-- conf
|
||||
# | |-- install.sh
|
||||
# | |-- com.google.santad.plist
|
||||
# | |-- com.google.santasync.plist
|
||||
# | |-- com.google.santagui.plist
|
||||
# | +-- com.google.santa.asl.conf
|
||||
# +--dsym
|
||||
@@ -43,7 +42,6 @@ PACKAGE_DOWNLOAD_URL:="https://github.com/google/santa/releases/download/${PACKA
|
||||
PAYLOAD:=pack-Library-Extensions-santa-driver.kext \
|
||||
pack-applications-Santa.app \
|
||||
pack-Library-LaunchDaemons-com.google.santad.plist \
|
||||
pack-Library-LaunchDaemons-com.google.santasync.plist \
|
||||
pack-Library-LaunchAgents-com.google.santagui.plist \
|
||||
pack-etc-asl-com.google.santa.asl.conf \
|
||||
pack-script-preinstall \
|
||||
@@ -53,7 +51,6 @@ santa-driver.kext: download
|
||||
Santa.app: download
|
||||
com.google.santad.plist: download
|
||||
com.google.santagui.plist: download
|
||||
com.google.santasync.plist: download
|
||||
com.google.santa.asl.conf: download
|
||||
|
||||
download:
|
||||
@@ -84,5 +81,4 @@ myclean:
|
||||
@rm -f com.google.santa.asl.conf
|
||||
@rm -f com.google.santad.plist
|
||||
@rm -f com.google.santagui.plist
|
||||
@rm -f com.google.santasync.plist
|
||||
@rm -f install.sh
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
sleep 1
|
||||
|
||||
/bin/launchctl load -w /Library/LaunchDaemons/com.google.santad.plist
|
||||
/bin/launchctl load -w /Library/LaunchDaemons/com.google.santasync.plist
|
||||
|
||||
sleep 1
|
||||
|
||||
|
||||
@@ -7,15 +7,16 @@
|
||||
[[ $3 != "/" ]] && exit 0
|
||||
|
||||
/bin/launchctl remove com.google.santad
|
||||
/bin/launchctl remove com.google.santasync
|
||||
|
||||
sleep 1
|
||||
|
||||
/sbin/kextunload -b com.google.santa-driver >/dev/null 2>&1
|
||||
|
||||
# Remove files from old Santa install locations, if they still exist
|
||||
# Remove cruft from old Santa versions
|
||||
/bin/rm /usr/libexec/santad
|
||||
/bin/rm /usr/sbin/santactl
|
||||
/bin/launchctl remove com.google.santasync
|
||||
/bin/rm /Library/LaunchDaemons/com.google.santasync.plist
|
||||
|
||||
sleep 1
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# Copy this file to /etc/asl to log all messages from santa-driver to the log file
|
||||
? [S= Message santa-driver:] claim only
|
||||
? [S= Message santa-driver:] file /var/log/santa.log format="[$((Time)(utc.3))] $Message"
|
||||
> /var/log/santa.log mode=0644 rotate=seq compress file_max=5M all_max=100M
|
||||
> /var/log/santa.log format="[$((Time)(utc.3))] $Message" mode=0644 rotate=seq compress file_max=10M all_max=100M uid=0 gid=0
|
||||
? [S= Message santa-driver:] claim
|
||||
? [S= Message santa-driver:] file /var/log/santa.log
|
||||
? [= Sender santad] claim
|
||||
? [= Sender santad] file /var/log/santa.log
|
||||
? [= Sender santactl] claim
|
||||
? [= Sender santactl] file /var/log/santa.log
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
<key>SantaXPCControl</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/var/log/santa.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/var/log/santa.log</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
|
||||
@@ -10,5 +10,7 @@
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.google.santasync</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Library/Extensions/santa-driver.kext/Contents/MacOS/santactl</string>
|
||||
<string>sync</string>
|
||||
</array>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/var/log/santa.log</string>
|
||||
<key>ProcessType</key>
|
||||
<string>Background</string>
|
||||
<key>StartInterval</key>
|
||||
<integer>600</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -19,7 +19,6 @@ GUI_USER=$(/usr/bin/stat -f '%u' /dev/console)
|
||||
|
||||
# Unload santad and scheduled sync job.
|
||||
/bin/launchctl remove com.google.santad >/dev/null 2>&1
|
||||
/bin/launchctl remove com.google.santasync >/dev/null 2>&1
|
||||
|
||||
# Unload kext.
|
||||
/sbin/kextunload -b com.google.santa-driver >/dev/null 2>&1
|
||||
@@ -28,16 +27,18 @@ GUI_USER=$(/usr/bin/stat -f '%u' /dev/console)
|
||||
[[ -n "$GUI_USER" ]] && \
|
||||
/bin/launchctl asuser ${GUI_USER} /bin/launchctl remove /Library/LaunchAgents/com.google.santagui.plist
|
||||
|
||||
# Delete old files, if they still exist
|
||||
/bin/rm /usr/libexec/santad
|
||||
/bin/rm /usr/sbin/santactl
|
||||
# Cleanup cruft from old versions
|
||||
/bin/launchctl remove com.google.santasync >/dev/null 2>&1
|
||||
/bin/rm /Library/LaunchDaemons/com.google.santasync.plist >/dev/null 2>&1
|
||||
/bin/rm /usr/libexec/santad >/dev/null 2>&1
|
||||
/bin/rm /usr/sbin/santactl >/dev/null 2>&1
|
||||
|
||||
# Copy new files.
|
||||
/bin/cp -r ${SOURCE}/binaries/santa-driver.kext /Library/Extensions
|
||||
/bin/cp -r ${SOURCE}/binaries/Santa.app /Applications
|
||||
/bin/ln -s /Library/Extensions/santa-driver.kext/Contents/MacOS/santactl /usr/local/bin
|
||||
|
||||
/bin/cp ${SOURCE}/conf/com.google.{santad,santasync}.plist /Library/LaunchDaemons
|
||||
/bin/cp ${SOURCE}/conf/com.google.santad.plist /Library/LaunchDaemons
|
||||
/bin/cp ${SOURCE}/conf/com.google.santagui.plist /Library/LaunchAgents
|
||||
/bin/cp ${SOURCE}/conf/com.google.santa.asl.conf /etc/asl/
|
||||
|
||||
@@ -49,7 +50,6 @@ GUI_USER=$(/usr/bin/stat -f '%u' /dev/console)
|
||||
|
||||
# Load santad and scheduled sync jobs.
|
||||
/bin/launchctl load /Library/LaunchDaemons/com.google.santad.plist
|
||||
/bin/launchctl load /Library/LaunchDaemons/com.google.santasync.plist
|
||||
|
||||
# Load GUI agent if someone is logged in.
|
||||
[[ -n "$GUI_USER" ]] && \
|
||||
|
||||
5
Podfile
5
Podfile
@@ -5,12 +5,13 @@ inhibit_all_warnings!
|
||||
target :santad do
|
||||
pod 'FMDB'
|
||||
|
||||
post_install do |rep|
|
||||
rep.project.targets.each do |target|
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
if config.name != 'Release' then
|
||||
break
|
||||
end
|
||||
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ''
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] <<= "NDEBUG=1"
|
||||
end
|
||||
|
||||
@@ -14,4 +14,4 @@ SPEC CHECKSUMS:
|
||||
FMDB: 96e8f1bcc1329e269330f99770ad4285d9003e52
|
||||
OCMock: a10ea9f0a6e921651f96f78b6faee95ebc813b92
|
||||
|
||||
COCOAPODS: 0.36.3
|
||||
COCOAPODS: 0.38.0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Santa [](https://travis-ci.org/google/santa)
|
||||
=====
|
||||
|
||||
Santa is a binary whitelisting/blacklisting system for Mac OS X. It consists of
|
||||
Santa is a binary whitelisting/blacklisting system for OS X. It consists of
|
||||
a kernel extension that monitors for executions, a userland daemon that makes
|
||||
execution decisions based on the contents of a SQLite database, a GUI agent that
|
||||
notifies the user in case of a block decision and a command-line utility for
|
||||
|
||||
31
Rakefile
31
Rakefile
@@ -1,10 +1,9 @@
|
||||
require 'timeout'
|
||||
|
||||
WORKSPACE = 'Santa.xcworkspace'
|
||||
DEFAULT_SCHEME = 'All'
|
||||
OUTPUT_PATH = 'Build'
|
||||
DIST_PATH = 'Dist'
|
||||
BINARIES = ['Santa.app', 'santa-driver.kext']
|
||||
DSYMS = ['Santa.app.dSYM', 'santa-driver.kext.dSYM', 'santad.dSYM', 'santactl.dSYM']
|
||||
XCPRETTY_DEFAULTS = '-sc'
|
||||
XCODEBUILD_DEFAULTS = "-workspace #{WORKSPACE} -derivedDataPath #{OUTPUT_PATH} -parallelizeTargets"
|
||||
|
||||
@@ -80,7 +79,6 @@ namespace :install do
|
||||
task :install, [:configuration] do |t, args|
|
||||
config = args[:configuration]
|
||||
system 'sudo cp conf/com.google.santad.plist /Library/LaunchDaemons'
|
||||
system 'sudo cp conf/com.google.santasync.plist /Library/LaunchDaemons'
|
||||
system 'sudo cp conf/com.google.santagui.plist /Library/LaunchAgents'
|
||||
system 'sudo cp conf/com.google.santa.asl.conf /etc/asl'
|
||||
Rake::Task['build:build'].invoke(config)
|
||||
@@ -95,6 +93,7 @@ end
|
||||
task :dist do
|
||||
desc "Create distribution folder"
|
||||
|
||||
Rake::Task['clean'].invoke()
|
||||
Rake::Task['build:build'].invoke("Release")
|
||||
|
||||
FileUtils.rm_rf(DIST_PATH)
|
||||
@@ -105,9 +104,13 @@ task :dist do
|
||||
|
||||
BINARIES.each do |x|
|
||||
FileUtils.cp_r("#{OUTPUT_PATH}/Products/Release/#{x}", "#{DIST_PATH}/binaries")
|
||||
FileUtils.cp_r("#{OUTPUT_PATH}/Products/Release/#{x}.dSYM", "#{DIST_PATH}/dsym")
|
||||
end
|
||||
|
||||
DSYMS.each do |x|
|
||||
FileUtils.cp_r("#{OUTPUT_PATH}/Products/Release/#{x}", "#{DIST_PATH}/dsym")
|
||||
end
|
||||
|
||||
|
||||
Dir.glob("Conf/*") {|x| File.directory?(x) or FileUtils.cp(x, "#{DIST_PATH}/conf")}
|
||||
|
||||
puts "Distribution folder created"
|
||||
@@ -126,16 +129,16 @@ namespace :tests do
|
||||
Rake::Task['unload'].invoke()
|
||||
Rake::Task['install:debug'].invoke()
|
||||
Rake::Task['load_kext'].invoke
|
||||
timeout = 30
|
||||
puts "Running kernel tests with a #{timeout} second timeout"
|
||||
FileUtils.mkdir_p("/tmp/santa_kerneltests_tmp")
|
||||
begin
|
||||
Timeout::timeout(timeout) {
|
||||
system "sudo #{OUTPUT_PATH}/Products/Debug/KernelTests"
|
||||
}
|
||||
rescue Timeout::Error
|
||||
puts "ERROR: tests ran for longer than #{timeout} seconds and were killed."
|
||||
puts "\033[?25l\033[12h" # hide cursor
|
||||
puts "Running kernel tests"
|
||||
system "cd /tmp/santa_kerneltests_tmp && sudo #{Dir.pwd}/#{OUTPUT_PATH}/Products/Debug/KernelTests"
|
||||
rescue Exception
|
||||
puts "\033[?25h\033[12l\n\n" # unhide cursor
|
||||
FileUtils.rm_rf("/tmp/santa_kerneltests_tmp")
|
||||
Rake::Task['unload_kext'].execute
|
||||
end
|
||||
Rake::Task['unload_kext'].execute
|
||||
end
|
||||
end
|
||||
|
||||
@@ -152,7 +155,7 @@ end
|
||||
|
||||
task :unload_gui do
|
||||
puts "Unloading GUI agent"
|
||||
system "sudo killall Santa 2>/dev/null"
|
||||
system "launchctl unload /Library/LaunchAgents/com.google.santagui.plist 2>/dev/null"
|
||||
end
|
||||
|
||||
desc "Unload"
|
||||
@@ -170,7 +173,7 @@ end
|
||||
|
||||
task :load_gui do
|
||||
puts "Loading GUI agent"
|
||||
system "open /Applications/Santa.app"
|
||||
system "launchctl load /Library/LaunchAgents/com.google.santagui.plist 2>/dev/null"
|
||||
end
|
||||
|
||||
desc "Load"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6254" systemVersion="14D136" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6254" systemVersion="14E46" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6254"/>
|
||||
@@ -37,7 +37,7 @@
|
||||
<rect key="frame" x="18" y="65" width="444" height="60"/>
|
||||
<textFieldCell key="cell" sendsActionOnEndEditing="YES" alignment="center" id="CcT-ul-1eA">
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="title">Santa is an application whitelisting system for Mac OS X.
|
||||
<string key="title">Santa is an application whitelisting system for OS X.
|
||||
|
||||
There are no user-configurable settings.</string>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6254" systemVersion="14D136" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6254" systemVersion="14E46" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6254"/>
|
||||
@@ -34,7 +34,7 @@
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="450" id="XgJ-EV-tBa"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="center" title="A message to the user goes here..." allowsEditingTextAttributes="YES" id="5tH-bG-UJA">
|
||||
<textFieldCell key="cell" selectable="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" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -137,7 +137,7 @@
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="290" id="on6-pj-m2k"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Executing User" id="HRT-Be-ePf">
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" title="Executing User" id="HRT-Be-ePf">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" white="0.0" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -218,15 +218,20 @@ DQ
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="290" id="h3Y-mO-38F"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Parent Name" id="ieo-WK-aDD">
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" title="Parent Name" id="ieo-WK-aDD">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" white="0.0" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.parentName" id="arL-Mc-4xj">
|
||||
<binding destination="-2" name="displayPatternValue1" keyPath="self.event.parentName" id="Lce-TO-q9V">
|
||||
<dictionary key="options">
|
||||
<string key="NSNullPlaceholder">Unknown</string>
|
||||
<string key="NSDisplayPattern">%{value1}@ (%{value2}@)</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
<binding destination="-2" name="displayPatternValue2" keyPath="self.event.ppid" previousBinding="Lce-TO-q9V" id="ofI-kH-F2d">
|
||||
<dictionary key="options">
|
||||
<string key="NSDisplayPattern">%{value1}@ (%{value2}@)</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
|
||||
@@ -133,8 +133,11 @@
|
||||
if ([self.customMessage length] > 0) {
|
||||
message = self.customMessage;
|
||||
} else {
|
||||
message = @"The following application has been blocked from executing<br />"
|
||||
@"because its trustworthiness cannot be determined.";
|
||||
message = [[SNTConfigurator configurator] defaultBlockMessage];
|
||||
if (!message) {
|
||||
message = @"The following application has been blocked from executing<br />"
|
||||
@"because its trustworthiness cannot be determined.";
|
||||
}
|
||||
}
|
||||
|
||||
NSString *fullHTML = [NSString stringWithFormat:@"%@%@%@", htmlHeader, message, htmlFooter];
|
||||
|
||||
@@ -35,6 +35,15 @@ extern NSString * const kDefaultConfigFilePath;
|
||||
///
|
||||
@property(nonatomic) BOOL logAllEvents;
|
||||
|
||||
///
|
||||
/// The regex of whitelisted paths. Regexes are specified in ICU format.
|
||||
///
|
||||
/// The regex flags IXSM can be used, though the s (dotall) and m (multiline) flags are
|
||||
/// pointless as a path only ever has a single line.
|
||||
/// If the regex doesn't begin with ^ to match from the beginning of the line, it will be added.
|
||||
///
|
||||
@property(nonatomic) NSRegularExpression *whitelistPathRegex;
|
||||
|
||||
#pragma mark - GUI Settings
|
||||
|
||||
///
|
||||
@@ -64,6 +73,12 @@ extern NSString * const kDefaultConfigFilePath;
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *eventDetailText;
|
||||
|
||||
///
|
||||
/// For any rule that doesn't have a custom message, this setting overrides the message
|
||||
/// text that is display. If unset, a reasonable default is provided.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *defaultBlockMessage;
|
||||
|
||||
#pragma mark - Sync Settings
|
||||
|
||||
///
|
||||
@@ -71,6 +86,12 @@ extern NSString * const kDefaultConfigFilePath;
|
||||
///
|
||||
@property(readonly, nonatomic) NSURL *syncBaseURL;
|
||||
|
||||
///
|
||||
/// If YES, mid-execution event uploads are skipped.
|
||||
/// This property is never stored on disk.
|
||||
///
|
||||
@property BOOL syncBackOff;
|
||||
|
||||
///
|
||||
/// The machine owner.
|
||||
///
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
@interface SNTConfigurator ()
|
||||
@property NSString *configFilePath;
|
||||
@property NSMutableDictionary *configData;
|
||||
|
||||
/// Creating NSRegularExpression objects is not fast, so cache it.
|
||||
@property NSRegularExpression *cachedWhitelistDirRegex;
|
||||
|
||||
/// Array of keys that cannot be changed while santad is running if santad didn't make the change.
|
||||
@property(readonly) NSArray *protectedKeys;
|
||||
@end
|
||||
|
||||
@implementation SNTConfigurator
|
||||
@@ -29,12 +35,13 @@ NSString * const kDefaultConfigFilePath = @"/var/db/santa/config.plist";
|
||||
|
||||
/// The keys in the config file
|
||||
static NSString * const kClientModeKey = @"ClientMode";
|
||||
|
||||
static NSString * const kWhitelistRegexKey = @"WhitelistRegex";
|
||||
static NSString * const kLogAllEventsKey = @"LogAllEvents";
|
||||
|
||||
static NSString * const kMoreInfoURLKey = @"MoreInfoURL";
|
||||
static NSString * const kEventDetailURLKey = @"EventDetailURL";
|
||||
static NSString * const kEventDetailTextKey = @"EventDetailText";
|
||||
static NSString * const kDefaultBlockMessage = @"DefaultBlockMessage";
|
||||
|
||||
static NSString * const kSyncBaseURLKey = @"SyncBaseURL";
|
||||
static NSString * const kClientAuthCertificateFileKey = @"ClientAuthCertificateFile";
|
||||
@@ -73,6 +80,12 @@ static NSString * const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
return sharedConfigurator;
|
||||
}
|
||||
|
||||
#pragma mark Protected Keys
|
||||
|
||||
- (NSArray *)protectedKeys {
|
||||
return @[ kClientModeKey, kWhitelistRegexKey ];
|
||||
}
|
||||
|
||||
#pragma mark Public Interface
|
||||
|
||||
- (santa_clientmode_t)clientMode {
|
||||
@@ -92,6 +105,27 @@ static NSString * const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
}
|
||||
}
|
||||
|
||||
- (NSRegularExpression *)whitelistPathRegex {
|
||||
if (!self.cachedWhitelistDirRegex && self.configData[kWhitelistRegexKey]) {
|
||||
NSString *re = self.configData[kWhitelistRegexKey];
|
||||
if (![re hasPrefix:@"^"]) re = [@"^" stringByAppendingString:re];
|
||||
self.cachedWhitelistDirRegex = [NSRegularExpression regularExpressionWithPattern:re
|
||||
options:0
|
||||
error:nil];
|
||||
}
|
||||
return self.cachedWhitelistDirRegex;
|
||||
}
|
||||
|
||||
- (void)setWhitelistPathRegex:(NSRegularExpression *)re {
|
||||
if (!re) {
|
||||
[self.configData removeObjectForKey:kWhitelistRegexKey];
|
||||
} else {
|
||||
self.configData[kWhitelistRegexKey] = [re pattern];
|
||||
}
|
||||
self.cachedWhitelistDirRegex = nil;
|
||||
[self saveConfigToDisk];
|
||||
}
|
||||
|
||||
- (BOOL)logAllEvents {
|
||||
return [self.configData[kLogAllEventsKey] boolValue];
|
||||
}
|
||||
@@ -113,6 +147,10 @@ static NSString * const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
return self.configData[kEventDetailTextKey];
|
||||
}
|
||||
|
||||
- (NSString *)defaultBlockMessage {
|
||||
return self.configData[kDefaultBlockMessage];
|
||||
}
|
||||
|
||||
- (NSURL *)syncBaseURL {
|
||||
return [NSURL URLWithString:self.configData[kSyncBaseURLKey]];
|
||||
}
|
||||
@@ -204,17 +242,20 @@ static NSString * const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure no-one is trying to change the client mode behind Santa's back.
|
||||
if (self.configData[kClientModeKey] && configData[kClientModeKey] &&
|
||||
![self.configData[kClientModeKey] isEqual:configData[kClientModeKey]] &&
|
||||
geteuid() == 0) {
|
||||
NSMutableDictionary *configDataMutable = [configData mutableCopy];
|
||||
configDataMutable[kClientModeKey] = self.configData[kClientModeKey];
|
||||
self.configData = configDataMutable;
|
||||
[self saveConfigToDisk];
|
||||
} else {
|
||||
self.configData = [configData mutableCopy];
|
||||
// Ensure no-one is trying to change protected keys behind our back.
|
||||
NSMutableDictionary *configDataMutable = [configData mutableCopy];
|
||||
BOOL changed = NO;
|
||||
for (NSString *key in self.protectedKeys) {
|
||||
if (self.configData[key] && configData[key] &&
|
||||
![self.configData[key] isEqual:configData[key]] && geteuid() == 0) {
|
||||
NSMutableDictionary *configDataMutable = [configData mutableCopy];
|
||||
configDataMutable[key] = self.configData[key];
|
||||
changed = YES;
|
||||
LOGD(@"Ignoring changed configuration key: %@", key);
|
||||
}
|
||||
}
|
||||
self.configData = configDataMutable;
|
||||
if (changed) [self saveConfigToDisk];
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
@@ -23,6 +23,16 @@
|
||||
///
|
||||
/// @param path The path of the file this instance is to represent. The path will be
|
||||
/// converted to an absolute, standardized path if it isn't already.
|
||||
/// @param error If an error occurred and nil is returned, this will be a pointer to an NSError
|
||||
/// describing the problem.
|
||||
///
|
||||
- (instancetype)initWithPath:(NSString *)path error:(NSError **)error;
|
||||
|
||||
///
|
||||
/// Convenience initializer.
|
||||
///
|
||||
/// @param path The path to the file this instance is to represent. The path will be
|
||||
/// converted to an absolute, standardized path if it isn't already.
|
||||
///
|
||||
- (instancetype)initWithPath:(NSString *)path;
|
||||
|
||||
|
||||
@@ -33,18 +33,34 @@
|
||||
|
||||
@implementation SNTFileInfo
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path {
|
||||
- (instancetype)initWithPath:(NSString *)path error:(NSError **)error {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_path = [self resolvePath:path];
|
||||
if (!_path) return nil;
|
||||
_fileData = [NSData dataWithContentsOfFile:_path options:NSDataReadingMappedIfSafe error:nil];
|
||||
if (_path.length == 0) {
|
||||
if (error) {
|
||||
NSString *errStr = @"Unable to resolve empty path";
|
||||
if (path) errStr = [@"Unable to resolve path: " stringByAppendingString:path];
|
||||
*error = [NSError errorWithDomain:@"com.google.santa.fileinfo"
|
||||
code:260
|
||||
userInfo:@{ NSLocalizedDescriptionKey: errStr }];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
_fileData = [NSData dataWithContentsOfFile:_path
|
||||
options:NSDataReadingMappedIfSafe
|
||||
error:error];
|
||||
if (_fileData.length == 0) return nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path {
|
||||
return [self initWithPath:path error:NULL];
|
||||
}
|
||||
|
||||
- (NSString *)SHA1 {
|
||||
unsigned char sha1[CC_SHA1_DIGEST_LENGTH];
|
||||
CC_SHA1(self.fileData.bytes, (unsigned int)self.fileData.length, sha1);
|
||||
@@ -207,15 +223,15 @@
|
||||
if (!self.infoDict) {
|
||||
self.infoDict = (NSDictionary *)[NSNull null];
|
||||
|
||||
if ([self bundle] && [self.bundle infoDictionary]) {
|
||||
// Binaries with embedded Info.plist aren't in an NSBundle but
|
||||
// CFBundleCopyInfoDictionaryForURL will return the embedded info dict.
|
||||
NSURL *url = [NSURL fileURLWithPath:self.path isDirectory:NO];
|
||||
NSDictionary *infoDict =
|
||||
(__bridge_transfer NSDictionary *)CFBundleCopyInfoDictionaryForURL((__bridge CFURLRef)url);
|
||||
if (infoDict){
|
||||
self.infoDict = infoDict;
|
||||
} else if ([self bundle] && [self.bundle infoDictionary]) {
|
||||
self.infoDict = [self.bundle infoDictionary];
|
||||
} else {
|
||||
// Binaries with embedded Info.plist aren't in an NSBundle but
|
||||
// CFBundleCopyInfoDictionaryForURL will return the embedded info dict.
|
||||
NSURL *url = [NSURL fileURLWithPath:self.path isDirectory:NO];
|
||||
NSDictionary *infoDict =
|
||||
(__bridge_transfer NSDictionary *)CFBundleCopyInfoDictionaryForURL((__bridge CFURLRef)url);
|
||||
if (infoDict) self.infoDict = infoDict;
|
||||
}
|
||||
}
|
||||
return self.infoDict == (NSDictionary *)[NSNull null] ? nil : self.infoDict;
|
||||
|
||||
@@ -39,19 +39,20 @@
|
||||
|
||||
///
|
||||
/// Logging function.
|
||||
///
|
||||
/// @param level one of the levels defined above
|
||||
/// @param destination a FILE, generally should be stdout or stderr
|
||||
/// @param destination a FILE, generally stdout/stderr. If the file is closed, the log
|
||||
/// will instead be sent to syslog.
|
||||
/// @param format the printf style format string
|
||||
/// @param ... the arguments to format.
|
||||
///
|
||||
void logMessage(int level, FILE *destination, NSString *format, ...);
|
||||
void logMessage(int level, FILE *destination, NSString *format, ...)
|
||||
__attribute__((format(__NSString__, 3, 4)));
|
||||
|
||||
/// Simple logging macros
|
||||
#define LOGD(logFormat, ...) logMessage(LOG_LEVEL_DEBUG, stdout, logFormat, ##__VA_ARGS__);
|
||||
#define LOGI(logFormat, ...) logMessage(LOG_LEVEL_INFO, stdout, logFormat, ##__VA_ARGS__);
|
||||
#define LOGW(logFormat, ...) logMessage(LOG_LEVEL_WARN, stderr, logFormat, ##__VA_ARGS__);
|
||||
#define LOGE(logFormat, ...) logMessage(LOG_LEVEL_ERROR, stderr, logFormat, ##__VA_ARGS__);
|
||||
#define LOGD(logFormat, ...) logMessage(LOG_LEVEL_DEBUG, stdout, logFormat, ##__VA_ARGS__)
|
||||
#define LOGI(logFormat, ...) logMessage(LOG_LEVEL_INFO, stdout, logFormat, ##__VA_ARGS__)
|
||||
#define LOGW(logFormat, ...) logMessage(LOG_LEVEL_WARN, stderr, logFormat, ##__VA_ARGS__)
|
||||
#define LOGE(logFormat, ...) logMessage(LOG_LEVEL_ERROR, stderr, logFormat, ##__VA_ARGS__)
|
||||
|
||||
#endif // KERNEL
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
#import "SNTLogging.h"
|
||||
|
||||
#import <sys/syslog.h>
|
||||
|
||||
#ifdef DEBUG
|
||||
static int logLevel = LOG_LEVEL_DEBUG;
|
||||
#else
|
||||
@@ -21,21 +23,23 @@ static int logLevel = LOG_LEVEL_INFO; // default to info
|
||||
#endif
|
||||
|
||||
void logMessage(int level, FILE *destination, NSString *format, ...) {
|
||||
static NSDateFormatter *dateFormatter;
|
||||
static BOOL useSyslog = NO;
|
||||
static NSString *binaryName;
|
||||
static dispatch_once_t pred;
|
||||
|
||||
dispatch_once(&pred, ^{
|
||||
dateFormatter = [[NSDateFormatter alloc] init];
|
||||
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
|
||||
[dateFormatter setDateFormat:@"YYYY-MM-dd HH:mm:ss.SSS'Z"];
|
||||
|
||||
binaryName = [[NSProcessInfo processInfo] processName];
|
||||
|
||||
// If debug logging is enabled, the process must be restarted.
|
||||
if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--debug"]) {
|
||||
logLevel = LOG_LEVEL_DEBUG;
|
||||
}
|
||||
|
||||
// If requested, redirect output to syslog.
|
||||
if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--syslog"] ||
|
||||
[binaryName isEqual:@"santad"]) {
|
||||
useSyslog = YES;
|
||||
}
|
||||
});
|
||||
|
||||
if (logLevel < level) return;
|
||||
@@ -45,19 +49,18 @@ void logMessage(int level, FILE *destination, NSString *format, ...) {
|
||||
NSString *s = [[NSString alloc] initWithFormat:format arguments:args];
|
||||
va_end(args);
|
||||
|
||||
// Only prepend timestamp, severity and binary name if stdout is not a TTY
|
||||
if (isatty(fileno(destination))) {
|
||||
fprintf(destination, "%s\n", [s UTF8String]);
|
||||
} else {
|
||||
if (useSyslog) {
|
||||
NSString *levelName;
|
||||
int syslogLevel = LOG_DEBUG;
|
||||
switch (level) {
|
||||
case LOG_LEVEL_ERROR: levelName = @"E"; break;
|
||||
case LOG_LEVEL_WARN: levelName = @"W"; break;
|
||||
case LOG_LEVEL_INFO: levelName = @"I"; break;
|
||||
case LOG_LEVEL_DEBUG: levelName = @"D"; break;
|
||||
case LOG_LEVEL_ERROR: levelName = @"E"; syslogLevel = LOG_ERR; break;
|
||||
case LOG_LEVEL_WARN: levelName = @"W"; syslogLevel = LOG_WARNING; break;
|
||||
case LOG_LEVEL_INFO: levelName = @"I"; syslogLevel = LOG_INFO; break;
|
||||
case LOG_LEVEL_DEBUG: levelName = @"D"; syslogLevel = LOG_DEBUG; break;
|
||||
}
|
||||
|
||||
fprintf(destination, "%s\n", [[NSString stringWithFormat:@"[%@] %@ %@: %@",
|
||||
[dateFormatter stringFromDate:[NSDate date]], levelName, binaryName, s] UTF8String]);
|
||||
syslog(syslogLevel, "%s\n",
|
||||
[[NSString stringWithFormat:@"%@ %@: %@", levelName, binaryName, s] UTF8String]);
|
||||
} else {
|
||||
fprintf(destination, "%s\n", [s UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,12 @@
|
||||
- (void)databaseRemoveEventsWithIDs:(NSArray *)ids;
|
||||
|
||||
///
|
||||
/// Misc ops
|
||||
/// Config ops
|
||||
///
|
||||
- (void)clientMode:(void (^)(santa_clientmode_t))reply;
|
||||
- (void)setClientMode:(santa_clientmode_t)mode reply:(void (^)())reply;
|
||||
- (void)setNextSyncInterval:(uint64_t)seconds reply:(void (^)())reply;
|
||||
- (void)setWhitelistPathRegex:(NSString *)pattern reply:(void (^)())reply;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -23,45 +23,35 @@ bool SantaDecisionManager::init() {
|
||||
if (!super::init()) return false;
|
||||
|
||||
sdm_lock_grp_ = lck_grp_alloc_init("santa-locks", lck_grp_attr_alloc_init());
|
||||
dataqueue_lock_ = lck_mtx_alloc_init(sdm_lock_grp_, lck_attr_alloc_init());
|
||||
cached_decisions_lock_ = lck_rw_alloc_init(sdm_lock_grp_,
|
||||
lck_attr_alloc_init());
|
||||
|
||||
cached_decisions_ = OSDictionary::withCapacity(1000);
|
||||
|
||||
dataqueue_ = IOSharedDataQueue::withCapacity((sizeof(santa_message_t) +
|
||||
DATA_QUEUE_ENTRY_HEADER_SIZE)
|
||||
* kMaxQueueEvents);
|
||||
dataqueue_ = IOSharedDataQueue::withEntries(kMaxQueueEvents,
|
||||
sizeof(santa_message_t));
|
||||
if (!dataqueue_) return kIOReturnNoMemory;
|
||||
|
||||
shared_memory_ = dataqueue_->getMemoryDescriptor();
|
||||
if (!shared_memory_) return kIOReturnNoMemory;
|
||||
|
||||
client_pid_ = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SantaDecisionManager::free() {
|
||||
if (shared_memory_) {
|
||||
shared_memory_->release();
|
||||
shared_memory_ = NULL;
|
||||
}
|
||||
|
||||
if (dataqueue_) {
|
||||
dataqueue_->release();
|
||||
dataqueue_ = NULL;
|
||||
}
|
||||
|
||||
if (cached_decisions_) {
|
||||
cached_decisions_->release();
|
||||
cached_decisions_ = NULL;
|
||||
}
|
||||
OSSafeReleaseNULL(dataqueue_);
|
||||
OSSafeReleaseNULL(cached_decisions_);
|
||||
|
||||
if (cached_decisions_lock_) {
|
||||
lck_rw_free(cached_decisions_lock_, sdm_lock_grp_);
|
||||
cached_decisions_lock_ = NULL;
|
||||
}
|
||||
|
||||
if (dataqueue_lock_) {
|
||||
lck_mtx_free(dataqueue_lock_, sdm_lock_grp_);
|
||||
dataqueue_lock_ = NULL;
|
||||
}
|
||||
|
||||
if (sdm_lock_grp_) {
|
||||
lck_grp_free(sdm_lock_grp_);
|
||||
sdm_lock_grp_ = NULL;
|
||||
@@ -79,10 +69,12 @@ void SantaDecisionManager::ConnectClient(mach_port_t port, pid_t pid) {
|
||||
// connected should be cleared
|
||||
ClearCache();
|
||||
|
||||
lck_mtx_lock(dataqueue_lock_);
|
||||
dataqueue_->setNotificationPort(port);
|
||||
lck_mtx_unlock(dataqueue_lock_);
|
||||
|
||||
client_pid_ = pid;
|
||||
client_proc_ = proc_find(pid);
|
||||
|
||||
failed_queue_requests_ = 0;
|
||||
}
|
||||
|
||||
@@ -95,12 +87,17 @@ void SantaDecisionManager::DisconnectClient(bool itDied) {
|
||||
if (!itDied) {
|
||||
santa_message_t message = {.action = ACTION_REQUEST_SHUTDOWN};
|
||||
PostToQueue(message);
|
||||
dataqueue_->setNotificationPort(NULL);
|
||||
} else {
|
||||
// If the client died, reset the data queue so when it reconnects
|
||||
// it doesn't get swamped straight away.
|
||||
lck_mtx_lock(dataqueue_lock_);
|
||||
dataqueue_->release();
|
||||
dataqueue_ = IOSharedDataQueue::withEntries(kMaxQueueEvents,
|
||||
sizeof(santa_message_t));
|
||||
lck_mtx_unlock(dataqueue_lock_);
|
||||
}
|
||||
|
||||
dataqueue_->setNotificationPort(NULL);
|
||||
|
||||
proc_rele(client_proc_);
|
||||
client_proc_ = NULL;
|
||||
}
|
||||
|
||||
bool SantaDecisionManager::ClientConnected() {
|
||||
@@ -108,7 +105,7 @@ bool SantaDecisionManager::ClientConnected() {
|
||||
}
|
||||
|
||||
IOMemoryDescriptor *SantaDecisionManager::GetMemoryDescriptor() {
|
||||
return shared_memory_;
|
||||
return dataqueue_->getMemoryDescriptor();
|
||||
}
|
||||
|
||||
#pragma mark Listener Control
|
||||
@@ -161,7 +158,7 @@ void SantaDecisionManager::AddToCache(
|
||||
// sufficiently large and a kMaxAllowCacheTimeMilliseconds set
|
||||
// sufficiently low, this should only ever occur if someone is purposefully
|
||||
// trying to make the cache grow.
|
||||
LOGD("Cache too large, flushing.");
|
||||
LOGI("Cache too large, flushing.");
|
||||
cached_decisions_->flushCollection();
|
||||
}
|
||||
|
||||
@@ -252,11 +249,14 @@ santa_action_t SantaDecisionManager::GetFromCache(const char *identifier) {
|
||||
}
|
||||
|
||||
santa_action_t SantaDecisionManager::GetFromDaemon(
|
||||
santa_message_t message, char *vnode_id_str) {
|
||||
const santa_message_t message, const char *vnode_id_str) {
|
||||
santa_action_t return_action = ACTION_UNSET;
|
||||
|
||||
// Wait for the daemon to respond or die.
|
||||
do {
|
||||
// Add pending request to cache.
|
||||
AddToCache(vnode_id_str, ACTION_REQUEST_CHECKBW, 0);
|
||||
|
||||
// Send request to daemon...
|
||||
if (!PostToQueue(message)) {
|
||||
OSIncrementAtomic(&failed_queue_requests_);
|
||||
@@ -270,15 +270,11 @@ santa_action_t SantaDecisionManager::GetFromDaemon(
|
||||
return ACTION_ERROR;
|
||||
}
|
||||
|
||||
// ... and wait for it to respond. If after kRequestLoopSleepMilliseconds
|
||||
// * kMaxRequestLoops it still hasn't responded, send request again.
|
||||
for (int i = 0; i < kMaxRequestLoops; ++i) {
|
||||
do {
|
||||
IOSleep(kRequestLoopSleepMilliseconds);
|
||||
return_action = GetFromCache(vnode_id_str);
|
||||
if (CHECKBW_RESPONSE_VALID(return_action)) break;
|
||||
}
|
||||
} while (!CHECKBW_RESPONSE_VALID(return_action) &&
|
||||
proc_exiting(client_proc_) == 0);
|
||||
} while (return_action == ACTION_REQUEST_CHECKBW && ClientConnected());
|
||||
} while (!CHECKBW_RESPONSE_VALID(return_action) && ClientConnected());
|
||||
|
||||
// If response is still not valid, the daemon exited
|
||||
if (!CHECKBW_RESPONSE_VALID(return_action)) {
|
||||
@@ -292,36 +288,29 @@ santa_action_t SantaDecisionManager::GetFromDaemon(
|
||||
}
|
||||
|
||||
santa_action_t SantaDecisionManager::FetchDecision(
|
||||
const kauth_cred_t credential,
|
||||
const vfs_context_t vfs_context,
|
||||
const vnode_t vnode) {
|
||||
const kauth_cred_t cred,
|
||||
const vnode_t vp,
|
||||
const uint64_t vnode_id,
|
||||
const char *vnode_id_str) {
|
||||
santa_action_t return_action = ACTION_UNSET;
|
||||
|
||||
// Fetch Vnode ID & string
|
||||
uint64_t vnode_id = GetVnodeIDForVnode(vfs_context, vnode);
|
||||
char vnode_id_str[MAX_VNODE_ID_STR];
|
||||
snprintf(vnode_id_str, MAX_VNODE_ID_STR, "%llu", vnode_id);
|
||||
|
||||
// Check to see if item is in cache
|
||||
return_action = GetFromCache(vnode_id_str);
|
||||
|
||||
// If item wasn in cache return it.
|
||||
if CHECKBW_RESPONSE_VALID(return_action) return return_action;
|
||||
|
||||
// Add pending request to cache.
|
||||
AddToCache(vnode_id_str, ACTION_REQUEST_CHECKBW, 0);
|
||||
|
||||
// Get path
|
||||
char path[MAXPATHLEN];
|
||||
int name_len = MAXPATHLEN;
|
||||
if (vn_getpath(vnode, path, &name_len) != 0) {
|
||||
if (vn_getpath(vp, path, &name_len) != 0) {
|
||||
path[0] = '\0';
|
||||
}
|
||||
|
||||
// Prepare message to send to daemon.
|
||||
santa_message_t message = {};
|
||||
strlcpy(message.path, path, sizeof(message.path));
|
||||
message.userId = kauth_cred_getuid(credential);
|
||||
message.userId = kauth_cred_getuid(cred);
|
||||
message.pid = proc_selfpid();
|
||||
message.ppid = proc_selfppid();
|
||||
message.action = ACTION_REQUEST_CHECKBW;
|
||||
@@ -341,16 +330,18 @@ santa_action_t SantaDecisionManager::FetchDecision(
|
||||
|
||||
bool SantaDecisionManager::PostToQueue(santa_message_t message) {
|
||||
bool kr = false;
|
||||
lck_mtx_lock(dataqueue_lock_);
|
||||
kr = dataqueue_->enqueue(&message, sizeof(message));
|
||||
lck_mtx_unlock(dataqueue_lock_);
|
||||
return kr;
|
||||
}
|
||||
|
||||
uint64_t SantaDecisionManager::GetVnodeIDForVnode(
|
||||
const vfs_context_t context, const vnode_t vp) {
|
||||
const vfs_context_t ctx, const vnode_t vp) {
|
||||
struct vnode_attr vap;
|
||||
VATTR_INIT(&vap);
|
||||
VATTR_WANTED(&vap, va_fileid);
|
||||
vnode_getattr(vp, &vap, context);
|
||||
vnode_getattr(vp, &vap, ctx);
|
||||
return vap.va_fileid;
|
||||
}
|
||||
|
||||
@@ -371,6 +362,54 @@ void SantaDecisionManager::DecrementListenerInvocations() {
|
||||
OSDecrementAtomic(&listener_invocations_);
|
||||
}
|
||||
|
||||
int SantaDecisionManager::VnodeCallback(const kauth_cred_t cred,
|
||||
const vfs_context_t ctx,
|
||||
const vnode_t vp,
|
||||
int *errno) {
|
||||
// Only operate on regular files (not directories, symlinks, etc.).
|
||||
vtype vt = vnode_vtype(vp);
|
||||
if (vt != VREG) return KAUTH_RESULT_DEFER;
|
||||
|
||||
// Get ID for the vnode and convert it to a string.
|
||||
uint64_t vnode_id = GetVnodeIDForVnode(ctx, vp);
|
||||
char vnode_str[MAX_VNODE_ID_STR];
|
||||
snprintf(vnode_str, MAX_VNODE_ID_STR, "%llu", vnode_id);
|
||||
|
||||
// Fetch decision
|
||||
santa_action_t returnedAction = FetchDecision(cred, vp, vnode_id, vnode_str);
|
||||
|
||||
// If file has dirty blocks, remove from cache and deny. This would usually
|
||||
// be the case if a file has been written to and flushed but not yet
|
||||
// closed.
|
||||
if (vnode_hasdirtyblks(vp)) {
|
||||
CacheCheck(vnode_str);
|
||||
returnedAction = ACTION_RESPOND_CHECKBW_DENY;
|
||||
}
|
||||
|
||||
switch (returnedAction) {
|
||||
case ACTION_RESPOND_CHECKBW_ALLOW:
|
||||
return KAUTH_RESULT_ALLOW;
|
||||
case ACTION_RESPOND_CHECKBW_DENY:
|
||||
*errno = EPERM;
|
||||
return KAUTH_RESULT_DENY;
|
||||
default:
|
||||
// NOTE: Any unknown response or error condition causes us to fail open.
|
||||
// Whilst from a security perspective this is bad, it's important that
|
||||
// we don't break user's machines.
|
||||
return KAUTH_RESULT_DEFER;
|
||||
}
|
||||
}
|
||||
|
||||
int SantaDecisionManager::FileOpCallback(const vnode_t vp) {
|
||||
vfs_context_t context = vfs_context_create(NULL);
|
||||
char vnode_id_str[MAX_VNODE_ID_STR];
|
||||
snprintf(vnode_id_str, MAX_VNODE_ID_STR, "%llu",
|
||||
GetVnodeIDForVnode(context, vp));
|
||||
CacheCheck(vnode_id_str);
|
||||
vfs_context_rele(context);
|
||||
return KAUTH_RESULT_DEFER;
|
||||
}
|
||||
|
||||
#undef super
|
||||
|
||||
#pragma mark Kauth Callbacks
|
||||
@@ -378,26 +417,17 @@ void SantaDecisionManager::DecrementListenerInvocations() {
|
||||
extern "C" int fileop_scope_callback(
|
||||
kauth_cred_t credential, void *idata, kauth_action_t action,
|
||||
uintptr_t arg0, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3) {
|
||||
if (!(action == KAUTH_FILEOP_CLOSE && arg2 & KAUTH_FILEOP_CLOSE_MODIFIED)) {
|
||||
return KAUTH_RESULT_DEFER;
|
||||
}
|
||||
|
||||
if (idata == NULL) {
|
||||
LOGE("FileOp callback established without valid decision manager.");
|
||||
if (action != KAUTH_FILEOP_CLOSE ||
|
||||
!(arg2 & KAUTH_FILEOP_CLOSE_MODIFIED) ||
|
||||
idata == NULL) {
|
||||
return KAUTH_RESULT_DEFER;
|
||||
}
|
||||
|
||||
SantaDecisionManager *sdm = OSDynamicCast(
|
||||
SantaDecisionManager, reinterpret_cast<OSObject *>(idata));
|
||||
|
||||
sdm->IncrementListenerInvocations();
|
||||
|
||||
vfs_context_t context = vfs_context_create(NULL);
|
||||
char vnode_id_str[MAX_VNODE_ID_STR];
|
||||
snprintf(vnode_id_str, MAX_VNODE_ID_STR, "%llu",
|
||||
sdm->GetVnodeIDForVnode(context, (vnode_t)arg0));
|
||||
sdm->CacheCheck(vnode_id_str);
|
||||
vfs_context_rele(context);
|
||||
|
||||
sdm->FileOpCallback(reinterpret_cast<vnode_t>(arg0));
|
||||
sdm->DecrementListenerInvocations();
|
||||
|
||||
return KAUTH_RESULT_DEFER;
|
||||
@@ -406,64 +436,20 @@ extern "C" int fileop_scope_callback(
|
||||
extern "C" int vnode_scope_callback(
|
||||
kauth_cred_t credential, void *idata, kauth_action_t action,
|
||||
uintptr_t arg0, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3) {
|
||||
// The default action is to defer
|
||||
int returnResult = KAUTH_RESULT_DEFER;
|
||||
|
||||
// Cast arguments to correct types
|
||||
if (idata == NULL) {
|
||||
LOGE("Vnode callback established without valid decision manager.");
|
||||
return returnResult;
|
||||
if (action & KAUTH_VNODE_ACCESS ||
|
||||
!(action & KAUTH_VNODE_EXECUTE) ||
|
||||
idata == NULL) {
|
||||
return KAUTH_RESULT_DEFER;
|
||||
}
|
||||
|
||||
SantaDecisionManager *sdm =
|
||||
OSDynamicCast(SantaDecisionManager, reinterpret_cast<OSObject *>(idata));
|
||||
vfs_context_t vfs_context = reinterpret_cast<vfs_context_t>(arg0);
|
||||
vnode_t vnode = reinterpret_cast<vnode_t>(arg1);
|
||||
|
||||
// Only operate on regular files (not directories, symlinks, etc.)
|
||||
vtype vt = vnode_vtype(vnode);
|
||||
if (vt != VREG) return returnResult;
|
||||
|
||||
// Don't operate on ACCESS events, as they're advisory
|
||||
if (action & KAUTH_VNODE_ACCESS) return returnResult;
|
||||
|
||||
// Filter for only EXECUTE actions
|
||||
if (action & KAUTH_VNODE_EXECUTE) {
|
||||
sdm->IncrementListenerInvocations();
|
||||
|
||||
// Fetch decision
|
||||
santa_action_t returnedAction =
|
||||
sdm->FetchDecision(credential, vfs_context, vnode);
|
||||
|
||||
// If file has dirty blocks, remove from cache and deny. This would usually
|
||||
// be the case if a file has been written to and flushed but not yet
|
||||
// closed.
|
||||
if (vnode_hasdirtyblks(vnode)) {
|
||||
char vnode_id_str[MAX_VNODE_ID_STR];
|
||||
snprintf(vnode_id_str, MAX_VNODE_ID_STR, "%llu",
|
||||
sdm->GetVnodeIDForVnode(vfs_context, vnode));
|
||||
sdm->CacheCheck(vnode_id_str);
|
||||
returnedAction = ACTION_RESPOND_CHECKBW_DENY;
|
||||
}
|
||||
|
||||
switch (returnedAction) {
|
||||
case ACTION_RESPOND_CHECKBW_ALLOW:
|
||||
returnResult = KAUTH_RESULT_ALLOW;
|
||||
break;
|
||||
case ACTION_RESPOND_CHECKBW_DENY:
|
||||
*(reinterpret_cast<int *>(arg3)) = EACCES;
|
||||
returnResult = KAUTH_RESULT_DENY;
|
||||
break;
|
||||
default:
|
||||
// NOTE: Any unknown response or error condition causes us to fail open.
|
||||
// Whilst from a security perspective this is bad, it's important that
|
||||
// we don't break user's machines.
|
||||
break;
|
||||
}
|
||||
|
||||
sdm->DecrementListenerInvocations();
|
||||
|
||||
return returnResult;
|
||||
}
|
||||
|
||||
return returnResult;
|
||||
sdm->IncrementListenerInvocations();
|
||||
int result = sdm->VnodeCallback(credential,
|
||||
reinterpret_cast<vfs_context_t>(arg0),
|
||||
reinterpret_cast<vnode_t>(arg1),
|
||||
reinterpret_cast<int *>(arg3));
|
||||
sdm->DecrementListenerInvocations();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -82,36 +82,29 @@ class SantaDecisionManager : public OSObject {
|
||||
/// Clears the cache.
|
||||
void ClearCache();
|
||||
|
||||
/// Fetches a response from the cache, first checking to see if the
|
||||
/// entry has expired.
|
||||
santa_action_t GetFromCache(const char *identifier);
|
||||
|
||||
/// Fetches a response from the daemon.
|
||||
santa_action_t GetFromDaemon(santa_message_t message, char *identifier);
|
||||
|
||||
/// Fetches an execution decision for a file, first using the cache and then
|
||||
/// by sending a message to the daemon and waiting until a response arrives.
|
||||
/// If a daemon isn't connected, will allow execution and cache, logging
|
||||
/// the path to the executed file.
|
||||
santa_action_t FetchDecision(const kauth_cred_t credential,
|
||||
const vfs_context_t vfs_context,
|
||||
const vnode_t vnode);
|
||||
|
||||
/// Posts the requested message to the client data queue.
|
||||
bool PostToQueue(santa_message_t);
|
||||
|
||||
/// Fetches the vnode_id for a given vnode.
|
||||
uint64_t GetVnodeIDForVnode(const vfs_context_t context, const vnode_t vp);
|
||||
|
||||
/// Returns the current system uptime in microseconds
|
||||
static uint64_t GetCurrentUptime();
|
||||
|
||||
/// Increments the count of active vnode callback's pending.
|
||||
void IncrementListenerInvocations();
|
||||
|
||||
/// Decrements the count of active vnode callback's pending.
|
||||
void DecrementListenerInvocations();
|
||||
|
||||
///
|
||||
/// Vnode Callback
|
||||
/// @param cred The kauth credential for this request.
|
||||
/// @param ctx The VFS context for this request.
|
||||
/// @param vp The Vnode for this request.
|
||||
/// @param errno A pointer to return an errno style error.
|
||||
/// @return int A valid KAUTH_RESULT_*.
|
||||
///
|
||||
int VnodeCallback(const kauth_cred_t cred, const vfs_context_t ctx,
|
||||
const vnode_t vp, int *errno);
|
||||
///
|
||||
/// FileOp Callback
|
||||
/// @param vp The Vnode for this request.
|
||||
/// @return int Should always be KAUTH_RESULT_DEFER.
|
||||
///
|
||||
int FileOpCallback(const vnode_t vp);
|
||||
|
||||
protected:
|
||||
///
|
||||
/// The maximum number of milliseconds a cached deny message should be
|
||||
@@ -131,12 +124,6 @@ class SantaDecisionManager : public OSObject {
|
||||
///
|
||||
const int kRequestLoopSleepMilliseconds = 10;
|
||||
|
||||
///
|
||||
/// While waiting for a response from the daemon, this is the maximum number
|
||||
/// of loops to wait before sending the request again.
|
||||
///
|
||||
const int kMaxRequestLoops = 50;
|
||||
|
||||
///
|
||||
/// Maximum number of entries in the in-kernel cache.
|
||||
///
|
||||
@@ -153,20 +140,69 @@ class SantaDecisionManager : public OSObject {
|
||||
///
|
||||
const int kMaxQueueEvents = 512;
|
||||
|
||||
/// Fetches a response from the cache, first checking to see if the
|
||||
/// entry has expired.
|
||||
santa_action_t GetFromCache(const char *identifier);
|
||||
|
||||
/// Fetches a response from the daemon. Handles both daemon death
|
||||
/// and failure to post messages to the daemon.
|
||||
///
|
||||
/// @param message The message to send to the daemon
|
||||
/// @param identifier The vnode ID string for this request
|
||||
/// @return santa_action_t The response for this request
|
||||
///
|
||||
santa_action_t GetFromDaemon(const santa_message_t message,
|
||||
const char *identifier);
|
||||
|
||||
///
|
||||
/// Fetches an execution decision for a file, first using the cache and then
|
||||
/// by sending a message to the daemon and waiting until a response arrives.
|
||||
/// If a daemon isn't connected, will allow execution and cache, logging
|
||||
/// the path to the executed file.
|
||||
///
|
||||
/// @param cred The credential for this request.
|
||||
/// @param vp The Vnode for this request.
|
||||
/// @param vnode_id The ID for this vnode.
|
||||
/// @param vnode_id_str A string representation of the above ID.
|
||||
///
|
||||
santa_action_t FetchDecision(const kauth_cred_t cred,
|
||||
const vnode_t vp,
|
||||
const uint64_t vnode_id,
|
||||
const char *vnode_id_str);
|
||||
|
||||
///
|
||||
/// Posts the requested message to the client data queue.
|
||||
///
|
||||
/// @param message The message to send
|
||||
/// @return bool true if sending was successful.
|
||||
///
|
||||
bool PostToQueue(santa_message_t message);
|
||||
|
||||
///
|
||||
/// Fetches the vnode_id for a given vnode.
|
||||
///
|
||||
/// @param ctx The VFS context to use.
|
||||
/// @param vp The Vnode to get the ID for
|
||||
/// @return uint64_t The Vnode ID as a 64-bit unsigned int.
|
||||
///
|
||||
uint64_t GetVnodeIDForVnode(const vfs_context_t ctx, const vnode_t vp);
|
||||
|
||||
/// Returns the current system uptime in microseconds
|
||||
static uint64_t GetCurrentUptime();
|
||||
|
||||
private:
|
||||
lck_grp_t *sdm_lock_grp_;
|
||||
lck_rw_t *cached_decisions_lock_;
|
||||
lck_mtx_t *dataqueue_lock_;
|
||||
|
||||
OSDictionary *cached_decisions_;
|
||||
|
||||
IOSharedDataQueue *dataqueue_;
|
||||
IOMemoryDescriptor *shared_memory_;
|
||||
|
||||
SInt32 failed_queue_requests_;
|
||||
|
||||
SInt32 listener_invocations_;
|
||||
|
||||
pid_t client_pid_;
|
||||
proc_t client_proc_;
|
||||
|
||||
kauth_listener_t vnode_listener_;
|
||||
kauth_listener_t fileop_listener_;
|
||||
|
||||
@@ -55,7 +55,7 @@ REGISTER_COMMAND_NAME(@"binaryinfo")
|
||||
|
||||
SNTFileInfo *fileInfo = [[SNTFileInfo alloc] initWithPath:filePath];
|
||||
if (!fileInfo) {
|
||||
printf("Invalid file\n");
|
||||
printf("Invalid or empty file\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,12 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@" --sha256 {sha256}: hash to add\n");
|
||||
}
|
||||
|
||||
+ (void)printErrorUsageAndExit:(NSString *)error {
|
||||
printf("%s\n\n", [error UTF8String]);
|
||||
printf("%s\n", [[self longHelpText] UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
+ (void)runWithArguments:(NSArray *)arguments daemonConnection:(SNTXPCConnection *)daemonConn {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
@@ -74,8 +80,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
|
||||
// add or remove
|
||||
if (!action) {
|
||||
printf("Missing action - add or remove?\n");
|
||||
exit(1);
|
||||
[self printErrorUsageAndExit:@"Missing action"];
|
||||
}
|
||||
|
||||
int state = RULESTATE_UNKNOWN;
|
||||
@@ -84,8 +89,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
} else if ([action compare:@"remove" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
|
||||
state = RULESTATE_REMOVE;
|
||||
} else {
|
||||
printf("Unknown action, expected add or remove.\n");
|
||||
exit(1);
|
||||
[self printErrorUsageAndExit:@"Unknown action"];
|
||||
}
|
||||
|
||||
NSString *customMsg = @"";
|
||||
@@ -103,46 +107,42 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
} else if ([argument compare:@"--silent-blacklist" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
|
||||
state = RULESTATE_SILENT_BLACKLIST;
|
||||
} else if ([argument compare:@"--message" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
|
||||
if (++i > ([arguments count])) {
|
||||
printf("No message specified.\n");
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"No message specified"];
|
||||
}
|
||||
|
||||
customMsg = [arguments objectAtIndex:i];
|
||||
} else if ([argument compare:@"--path" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
|
||||
if (++i > ([arguments count])) {
|
||||
printf("No path specified.\n");
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"No path specified"];
|
||||
}
|
||||
|
||||
filePath = [arguments objectAtIndex:i];
|
||||
} else if ([argument compare:@"--sha256" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
|
||||
if (++i > ([arguments count])) {
|
||||
printf("No SHA-256 specified.\n");
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"No SHA-256 specified"];
|
||||
}
|
||||
|
||||
SHA256 = [arguments objectAtIndex:i];
|
||||
} else {
|
||||
printf("Unknown argument %s.\n", [argument UTF8String]);
|
||||
exit(1);
|
||||
[self printErrorUsageAndExit:[@"Unknown argument: %@" stringByAppendingString:argument]];
|
||||
}
|
||||
}
|
||||
|
||||
if (state == RULESTATE_UNKNOWN) {
|
||||
printf("No state specified.\n");
|
||||
exit(1);
|
||||
[self printErrorUsageAndExit:@"No state specified"];
|
||||
}
|
||||
|
||||
if (filePath) {
|
||||
SNTFileInfo *fileInfo = [[SNTFileInfo alloc] initWithPath:filePath];
|
||||
if (!fileInfo) {
|
||||
printf("Not a regular file or executable bundle.\n");
|
||||
exit(1);
|
||||
[self printErrorUsageAndExit:@"Provided path is not a regular file or executable bundle"];
|
||||
}
|
||||
|
||||
SHA256 = [fileInfo SHA256];
|
||||
} else if (SHA256) {
|
||||
} else {
|
||||
printf("No SHA-256 or binary specified.\n");
|
||||
exit(1);
|
||||
[self printErrorUsageAndExit:@"Either SHA-256 or path to file must be specified"];
|
||||
}
|
||||
|
||||
SNTRule *newRule = [[SNTRule alloc] init];
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
|
||||
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
[config setTLSMinimumSupportedProtocol:kTLSProtocol12];
|
||||
[config setHTTPShouldUsePipelining:YES];
|
||||
return [self initWithSessionConfiguration:config];
|
||||
@@ -112,6 +112,8 @@
|
||||
newRequest:(NSURLRequest *)request
|
||||
completionHandler:(void (^)(NSURLRequest *))completionHandler {
|
||||
if (self.refusesRedirects) {
|
||||
LOGD(@"Rejected redirection to: %@", request.URL);
|
||||
[task cancel]; // without this, the connection hangs until timeout!?!
|
||||
completionHandler(NULL);
|
||||
} else {
|
||||
completionHandler(request);
|
||||
|
||||
@@ -46,7 +46,7 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
}
|
||||
|
||||
+ (NSString *)shortHelpText {
|
||||
return @"Synchronizes Santa with the server.";
|
||||
return @"Synchronizes Santa with a configured server.";
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
@@ -168,11 +168,11 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
completionHandler:^(BOOL success) {
|
||||
if (success) {
|
||||
LOGI(@"Log upload complete");
|
||||
[self eventUpload];
|
||||
} else {
|
||||
LOGE(@"Log upload failed, aborting run");
|
||||
exit(1);
|
||||
LOGE(@"Log upload failed, continuing anyway");
|
||||
}
|
||||
[self eventUpload];
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -198,11 +198,11 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
daemonConn:self.daemonConn
|
||||
completionHandler:^(BOOL success) {
|
||||
if (success) {
|
||||
LOGI(@"Event upload complete");
|
||||
exit(0);
|
||||
LOGI(@"Event upload complete");
|
||||
exit(0);
|
||||
} else {
|
||||
LOGW(@"Event upload failed");
|
||||
exit(1);
|
||||
LOGW(@"Event upload failed");
|
||||
exit(1);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ extern NSString * const kClientMode;
|
||||
extern NSString * const kClientModeMonitor;
|
||||
extern NSString * const kClientModeLockdown;
|
||||
extern NSString * const kCleanSync;
|
||||
extern NSString * const kWhitelistRegex;
|
||||
|
||||
extern NSString * const kEvents;
|
||||
extern NSString * const kFileSHA256;
|
||||
@@ -78,3 +79,5 @@ extern NSString * const kRuleTypeBinary;
|
||||
extern NSString * const kRuleTypeCertificate;
|
||||
extern NSString * const kRuleCustomMsg;
|
||||
extern NSString * const kCursor;
|
||||
|
||||
extern NSString * const kBackoffInterval;
|
||||
|
||||
@@ -32,6 +32,7 @@ NSString * const kClientMode = @"client_mode";
|
||||
NSString * const kClientModeMonitor = @"MONITOR";
|
||||
NSString * const kClientModeLockdown = @"LOCKDOWN";
|
||||
NSString * const kCleanSync = @"clean_sync";
|
||||
NSString * const kWhitelistRegex = @"whitelist_regex";
|
||||
|
||||
NSString * const kEvents = @"events";
|
||||
NSString * const kFileSHA256 = @"file_sha256";
|
||||
@@ -80,3 +81,5 @@ NSString * const kRuleTypeBinary = @"BINARY";
|
||||
NSString * const kRuleTypeCertificate = @"CERTIFICATE";
|
||||
NSString * const kRuleCustomMsg = @"custom_msg";
|
||||
NSString * const kCursor = @"cursor";
|
||||
|
||||
NSString * const kBackoffInterval = @"backoff";
|
||||
|
||||
@@ -104,12 +104,12 @@
|
||||
NSError *error) {
|
||||
long statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if (statusCode != 200) {
|
||||
LOGE(@"HTTP Response: %d %@",
|
||||
LOGE(@"HTTP Response: %ld %@",
|
||||
statusCode,
|
||||
[[NSHTTPURLResponse localizedStringForStatusCode:statusCode] capitalizedString]);
|
||||
handler(NO);
|
||||
} else {
|
||||
LOGI(@"Uploaded %d events", eventIds.count);
|
||||
LOGI(@"Uploaded %lu events", eventIds.count);
|
||||
|
||||
[[daemonConn remoteObjectProxy] databaseRemoveEventsWithIDs:eventIds];
|
||||
|
||||
|
||||
@@ -45,12 +45,12 @@
|
||||
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
long statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if (statusCode != 200) {
|
||||
LOGE(@"HTTP Response: %d %@",
|
||||
LOGE(@"HTTP Response: %ld %@",
|
||||
statusCode,
|
||||
[[NSHTTPURLResponse localizedStringForStatusCode:statusCode] capitalizedString]);
|
||||
handler(NO);
|
||||
} else {
|
||||
LOGI(@"Uploaded %d logs", [logsToUpload count]);
|
||||
LOGI(@"Uploaded %lu logs", [logsToUpload count]);
|
||||
handler(YES);
|
||||
}
|
||||
}] resume];
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
#import "SNTCommandSyncState.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@implementation SNTCommandSyncPostflight
|
||||
|
||||
@@ -33,15 +35,22 @@
|
||||
[[session dataTaskWithRequest:req completionHandler:^(NSData *data,
|
||||
NSURLResponse *response,
|
||||
NSError *error) {
|
||||
long statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if (statusCode != 200) {
|
||||
LOGE(@"HTTP Response: %d %@",
|
||||
statusCode,
|
||||
[[NSHTTPURLResponse localizedStringForStatusCode:statusCode] capitalizedString]);
|
||||
handler(NO);
|
||||
} else {
|
||||
handler(YES);
|
||||
long statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if (statusCode != 200) {
|
||||
LOGE(@"HTTP Response: %ld %@",
|
||||
statusCode,
|
||||
[[NSHTTPURLResponse localizedStringForStatusCode:statusCode] capitalizedString]);
|
||||
handler(NO);
|
||||
} else {
|
||||
NSDictionary *r = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
|
||||
NSString *backoffInterval = r[kBackoffInterval];
|
||||
if (backoffInterval) {
|
||||
[[daemonConn remoteObjectProxy] setNextSyncInterval:[backoffInterval intValue] reply:^{}];
|
||||
}
|
||||
|
||||
handler(YES);
|
||||
}
|
||||
}] resume];
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
NSError *error) {
|
||||
long statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if (statusCode != 200) {
|
||||
LOGE(@"HTTP Response: %d %@",
|
||||
LOGE(@"HTTP Response: %ld %@",
|
||||
statusCode,
|
||||
[[NSHTTPURLResponse localizedStringForStatusCode:statusCode] capitalizedString]);
|
||||
handler(NO);
|
||||
@@ -73,9 +73,12 @@
|
||||
[[daemonConn remoteObjectProxy] setClientMode:CLIENTMODE_LOCKDOWN reply:^{}];
|
||||
}
|
||||
|
||||
if ([r[kWhitelistRegex] isKindOfClass:[NSString class]]) {
|
||||
[[daemonConn remoteObjectProxy] setWhitelistPathRegex:r[kWhitelistRegex] reply:^{}];
|
||||
}
|
||||
|
||||
if ([r[kCleanSync] boolValue]) {
|
||||
syncState.cleanSync = YES;
|
||||
LOGD(@"Clean sync requested by server");
|
||||
}
|
||||
|
||||
handler(YES);
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
NSError *error) {
|
||||
long statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if (statusCode != 200) {
|
||||
LOGE(@"HTTP Response: %d %@",
|
||||
LOGE(@"HTTP Response: %ld %@",
|
||||
statusCode,
|
||||
[[NSHTTPURLResponse localizedStringForStatusCode:statusCode] capitalizedString]);
|
||||
handler(NO);
|
||||
@@ -91,7 +91,7 @@
|
||||
[[daemonConn remoteObjectProxy] databaseRuleAddRules:syncState.downloadedRules
|
||||
cleanSlate:syncState.cleanSync
|
||||
reply:^{
|
||||
LOGI(@"Added %d rule(s)", syncState.downloadedRules.count);
|
||||
LOGI(@"Added %lu rule(s)", syncState.downloadedRules.count);
|
||||
handler(YES);
|
||||
}];
|
||||
} else {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <pwd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
@@ -130,17 +129,7 @@
|
||||
// Validate the binary aynchronously on a concurrent queue so we don't
|
||||
// hold up other execution requests in the background.
|
||||
dispatch_async(q, ^{
|
||||
struct passwd *user = getpwuid(message.userId);
|
||||
NSString *userName;
|
||||
if (user) {
|
||||
userName = @(user->pw_name);
|
||||
}
|
||||
|
||||
[self.execController validateBinaryWithPath:@(message.path)
|
||||
userName:userName
|
||||
pid:@(message.pid)
|
||||
ppid:@(message.ppid)
|
||||
vnodeId:message.vnode_id];
|
||||
[self.execController validateBinaryWithMessage:message];
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -17,21 +17,61 @@
|
||||
#import "SNTConfigurator.h"
|
||||
#import "SNTDatabaseController.h"
|
||||
#import "SNTDriverManager.h"
|
||||
#import "SNTDropRootPrivs.h"
|
||||
#import "SNTEventTable.h"
|
||||
#import "SNTLogging.h"
|
||||
#import "SNTRule.h"
|
||||
#import "SNTRuleTable.h"
|
||||
|
||||
@interface SNTDaemonControlController ()
|
||||
@property dispatch_source_t syncTimer;
|
||||
@end
|
||||
|
||||
@implementation SNTDaemonControlController
|
||||
|
||||
- (instancetype)initWithDriverManager:(SNTDriverManager *)driverManager {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_driverManager = driverManager;
|
||||
|
||||
_syncTimer = [self createSyncTimer];
|
||||
[self rescheduleSyncSecondsFromNow:30];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (dispatch_source_t)createSyncTimer {
|
||||
dispatch_source_t syncTimerQ = dispatch_source_create(
|
||||
DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
|
||||
|
||||
dispatch_source_set_event_handler(syncTimerQ, ^{
|
||||
[self rescheduleSyncSecondsFromNow:600];
|
||||
|
||||
if (![[SNTConfigurator configurator] syncBaseURL]) return;
|
||||
[[SNTConfigurator configurator] setSyncBackOff:NO];
|
||||
|
||||
if (fork() == 0) {
|
||||
// Ensure we have no privileges
|
||||
if (!DropRootPrivileges()) {
|
||||
_exit(EPERM);
|
||||
}
|
||||
|
||||
_exit(execl(kSantaCtlPath, kSantaCtlPath, "sync", "--syslog", NULL));
|
||||
}
|
||||
});
|
||||
|
||||
dispatch_resume(syncTimerQ);
|
||||
|
||||
return syncTimerQ;
|
||||
}
|
||||
|
||||
- (void)rescheduleSyncSecondsFromNow:(uint64_t)seconds {
|
||||
uint64_t interval = seconds * NSEC_PER_SEC;
|
||||
uint64_t leeway = (seconds * 0.05) * NSEC_PER_SEC;
|
||||
dispatch_source_set_timer(self.syncTimer, dispatch_walltime(NULL, interval), interval, leeway);
|
||||
}
|
||||
|
||||
#pragma mark Kernel ops
|
||||
|
||||
- (void)cacheCount:(void (^)(int64_t))reply {
|
||||
@@ -94,4 +134,18 @@
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)setNextSyncInterval:(uint64_t)seconds reply:(void (^)())reply {
|
||||
[self rescheduleSyncSecondsFromNow:seconds];
|
||||
[[SNTConfigurator configurator] setSyncBackOff:YES];
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)setWhitelistPathRegex:(NSString *)pattern reply:(void (^)())reply {
|
||||
NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:pattern
|
||||
options:0
|
||||
error:NULL];
|
||||
[[SNTConfigurator configurator] setWhitelistPathRegex:re];
|
||||
reply();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -126,6 +126,9 @@
|
||||
for (NSNumber *index in indexes) {
|
||||
[self deleteEventWithId:index];
|
||||
}
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
[db executeUpdate:@"VACUUM"];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#include "SNTCommonEnums.h"
|
||||
#include "SNTKernelCommon.h"
|
||||
|
||||
@class SNTCodesignChecker;
|
||||
@class SNTDriverManager;
|
||||
@@ -48,16 +49,8 @@
|
||||
/// the kernel, logs the event to the log and if necessary stores the event in the database and
|
||||
/// sends a notification to the GUI agent.
|
||||
///
|
||||
/// @param path the binary that's being executed
|
||||
/// @param userName the user who's executing the binary
|
||||
/// @param pid the process id being executed
|
||||
/// @param ppid the parent process id
|
||||
/// @param vnodeId the id of the vnode being executed
|
||||
/// @param message The message sent from the kernel.
|
||||
///
|
||||
- (void)validateBinaryWithPath:(NSString *)path
|
||||
userName:(NSString *)userName
|
||||
pid:(NSNumber *)pid
|
||||
ppid:(NSNumber *)ppid
|
||||
vnodeId:(uint64_t)vnodeId;
|
||||
- (void)validateBinaryWithMessage:(santa_message_t)message;
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#import "SNTExecutionController.h"
|
||||
|
||||
#include <libproc.h>
|
||||
#include <pwd.h>
|
||||
#include <utmpx.h>
|
||||
|
||||
#include "SNTLogging.h"
|
||||
@@ -47,7 +48,6 @@
|
||||
_ruleTable = ruleTable;
|
||||
_eventTable = eventTable;
|
||||
_notifierConnection = notifier;
|
||||
LOGI(@"Log format: Decision (A|D), Reason (B|C|S|?), SHA-256, Path, Cert SHA-256, Cert CN");
|
||||
|
||||
// Workaround for xpcproxy/libsecurity bug on Yosemite
|
||||
// This establishes the XPC connection between libsecurity and syspolicyd.
|
||||
@@ -59,21 +59,29 @@
|
||||
|
||||
#pragma mark Binary Validation
|
||||
|
||||
- (void)validateBinaryWithPath:(NSString *)path
|
||||
userName:(NSString *)userName
|
||||
pid:(NSNumber *)pid
|
||||
ppid:(NSNumber *)ppid
|
||||
vnodeId:(uint64_t)vnodeId {
|
||||
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:path];
|
||||
- (void)validateBinaryWithMessage:(santa_message_t)message {
|
||||
NSString *path = @(message.path);
|
||||
uint64_t vnodeId = message.vnode_id;
|
||||
|
||||
NSError *fileInfoError;
|
||||
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:path error:&fileInfoError];
|
||||
NSString *sha256 = [binInfo SHA256];
|
||||
|
||||
// If we can't read the file and hash properly, log an error.
|
||||
if (!binInfo || !sha256) {
|
||||
LOGW(@"Failed to read file %@: %@", path, fileInfoError.localizedDescription);
|
||||
[self.driverManager postToKernelAction:ACTION_RESPOND_CHECKBW_ALLOW forVnodeID:vnodeId];
|
||||
[self logDecisionForEventState:EVENTSTATE_ALLOW_UNKNOWN sha256:nil path:path leafCert:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
// These will be filled in either in later steps
|
||||
santa_action_t respondedAction = ACTION_UNSET;
|
||||
SNTRule *rule;
|
||||
|
||||
// Get name of parent process. Do this before responding to be sure parent doesn't go away.
|
||||
char pname[PROC_PIDPATHINFO_MAXSIZE];
|
||||
proc_name([ppid intValue], pname, PROC_PIDPATHINFO_MAXSIZE);
|
||||
proc_name(message.ppid, pname, PROC_PIDPATHINFO_MAXSIZE);
|
||||
|
||||
// Step 1 - binary rule?
|
||||
rule = [self.ruleTable binaryRuleForSHA256:sha256];
|
||||
@@ -94,7 +102,7 @@
|
||||
}
|
||||
|
||||
// Step 3 - in scope?
|
||||
if (![self fileIsInScope:path]) {
|
||||
if (!rule && ![self fileIsInScope:path]) {
|
||||
[self.driverManager postToKernelAction:ACTION_RESPOND_CHECKBW_ALLOW forVnodeID:vnodeId];
|
||||
[self logDecisionForEventState:EVENTSTATE_ALLOW_SCOPE sha256:sha256 path:path leafCert:nil];
|
||||
return;
|
||||
@@ -125,13 +133,20 @@
|
||||
}
|
||||
|
||||
se.signingChain = csInfo.certificates;
|
||||
se.executingUser = userName;
|
||||
se.occurrenceDate = [[NSDate alloc] init];
|
||||
se.decision = [self eventStateForDecision:respondedAction type:rule.type];
|
||||
se.pid = pid;
|
||||
se.ppid = ppid;
|
||||
se.pid = @(message.pid);
|
||||
se.ppid = @(message.ppid);
|
||||
se.parentName = @(pname);
|
||||
|
||||
struct passwd *user = getpwuid(message.userId);
|
||||
endpwent();
|
||||
NSString *userName;
|
||||
if (user) {
|
||||
userName = @(user->pw_name);
|
||||
}
|
||||
se.executingUser = userName;
|
||||
|
||||
NSArray *loggedInUsers, *currentSessions;
|
||||
[self loggedInUsers:&loggedInUsers sessions:¤tSessions];
|
||||
se.currentSessions = currentSessions;
|
||||
@@ -144,7 +159,9 @@
|
||||
// upload for the blocked binary rather than waiting for the next sync.
|
||||
// The event upload is skipped if the full path is equal to that of santactl so that
|
||||
/// on the off chance that santactl is not whitelisted, we don't get into an infinite loop.
|
||||
if (![path isEqual:@(kSantaCtlPath)]) {
|
||||
if (![path isEqual:@(kSantaCtlPath)] &&
|
||||
[[SNTConfigurator configurator] syncBaseURL]
|
||||
&& ![[SNTConfigurator configurator] syncBackOff]) {
|
||||
[self initiateEventUploadForSHA256:sha256];
|
||||
}
|
||||
|
||||
@@ -167,13 +184,14 @@
|
||||
///
|
||||
/// Files that are out of scope:
|
||||
/// + Non Mach-O files that are not part of an installer package.
|
||||
/// + Files in whitelisted directories.
|
||||
/// + Files in whitelisted path.
|
||||
///
|
||||
/// @return @c YES if file is in scope, @c NO otherwise.
|
||||
///
|
||||
- (BOOL)fileIsInScope:(NSString *)path {
|
||||
// Determine if file is within a whitelisted directory.
|
||||
if ([self pathIsInWhitelistedDir:path]) {
|
||||
// Determine if file is within a whitelisted path
|
||||
NSRegularExpression *re = [[SNTConfigurator configurator] whitelistPathRegex];
|
||||
if ([re numberOfMatchesInString:path options:0 range:NSMakeRange(0, path.length)]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -188,30 +206,22 @@
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)pathIsInWhitelistedDir:(NSString *)path {
|
||||
// TODO(rah): Implement this.
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (santa_eventstate_t)eventStateForDecision:(santa_action_t)decision type:(santa_ruletype_t)type {
|
||||
if (decision == ACTION_RESPOND_CHECKBW_ALLOW) {
|
||||
if (type == RULETYPE_BINARY) {
|
||||
return EVENTSTATE_ALLOW_BINARY;
|
||||
} else if (type == RULETYPE_CERT) {
|
||||
return EVENTSTATE_ALLOW_CERTIFICATE;
|
||||
} else {
|
||||
return EVENTSTATE_ALLOW_UNKNOWN;
|
||||
}
|
||||
} else if (decision == ACTION_RESPOND_CHECKBW_DENY) {
|
||||
if (type == RULETYPE_BINARY) {
|
||||
return EVENTSTATE_BLOCK_BINARY;
|
||||
} else if (decision == RULETYPE_CERT) {
|
||||
return EVENTSTATE_BLOCK_CERTIFICATE;
|
||||
} else {
|
||||
return EVENTSTATE_BLOCK_UNKNOWN;
|
||||
}
|
||||
} else {
|
||||
return EVENTSTATE_UNKNOWN;
|
||||
switch (decision) {
|
||||
case ACTION_RESPOND_CHECKBW_ALLOW:
|
||||
switch (type) {
|
||||
case RULETYPE_BINARY: return EVENTSTATE_ALLOW_BINARY;
|
||||
case RULETYPE_CERT: return EVENTSTATE_ALLOW_CERTIFICATE;
|
||||
default: return EVENTSTATE_ALLOW_UNKNOWN;
|
||||
|
||||
}
|
||||
case ACTION_RESPOND_CHECKBW_DENY:
|
||||
switch (type) {
|
||||
case RULETYPE_BINARY: return EVENTSTATE_BLOCK_BINARY;
|
||||
case RULETYPE_CERT: return EVENTSTATE_BLOCK_CERTIFICATE;
|
||||
default: return EVENTSTATE_BLOCK_UNKNOWN;
|
||||
}
|
||||
default: return EVENTSTATE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,15 +269,10 @@
|
||||
}
|
||||
|
||||
- (void)initiateEventUploadForSHA256:(NSString *)sha256 {
|
||||
signal(SIGCHLD, SIG_IGN);
|
||||
pid_t child = fork();
|
||||
if (child == 0) {
|
||||
fclose(stdout);
|
||||
fclose(stderr);
|
||||
|
||||
if (fork() == 0) {
|
||||
// Ensure we have no privileges
|
||||
if (!DropRootPrivileges()) {
|
||||
_exit(1);
|
||||
_exit(EPERM);
|
||||
}
|
||||
|
||||
_exit(execl(kSantaCtlPath, kSantaCtlPath, "sync", "singleevent", [sha256 UTF8String], NULL));
|
||||
@@ -295,11 +300,10 @@
|
||||
}
|
||||
|
||||
- (void)loggedInUsers:(NSArray **)users sessions:(NSArray **)sessions {
|
||||
struct utmpx *nxt;
|
||||
|
||||
NSMutableDictionary *loggedInUsers = [[NSMutableDictionary alloc] init];
|
||||
NSMutableDictionary *loggedInHosts = [[NSMutableDictionary alloc] init];
|
||||
|
||||
struct utmpx *nxt;
|
||||
while ((nxt = getutxent())) {
|
||||
if (nxt->ut_type != USER_PROCESS) continue;
|
||||
|
||||
@@ -312,11 +316,11 @@
|
||||
sessionName = [NSString stringWithFormat:@"%s@%s", nxt->ut_user, nxt->ut_line];
|
||||
}
|
||||
|
||||
if ([userName length] > 0) {
|
||||
if (userName.length > 0) {
|
||||
loggedInUsers[userName] = [NSNull null];
|
||||
}
|
||||
|
||||
if ([sessionName length] > 1) {
|
||||
if (sessionName.length > 1) {
|
||||
loggedInHosts[sessionName] = [NSNull null];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,45 +26,49 @@ static inline double timeval_to_double(struct timeval tv) {
|
||||
|
||||
/// The watchdog thread function, used to monitor santad CPU/RAM usage and print a warning
|
||||
/// if it goes over certain thresholds.
|
||||
void *watchdog_thread_f(__unused void *idata) {
|
||||
pthread_setname_np("Watchdog");
|
||||
void *watchdogThreadFunction(__unused void *idata) {
|
||||
pthread_setname_np("com.google.santa.watchdog");
|
||||
|
||||
// Number of seconds to wait between checks.
|
||||
const int timeInterval = 60;
|
||||
|
||||
// Amount of CPU usage to trigger warning, as a percentage averaged over timeInterval
|
||||
// santad's usual CPU usage is 0-3% but can occasionally spike if lots of processes start at once.
|
||||
const int cpuWarnThreshold = 20;
|
||||
const int cpuWarnThreshold = 20.0;
|
||||
|
||||
// Amount of RAM usage to trigger warning, in MB.
|
||||
// santad's usual RAM usage is between 5-50MB but can spike if lots of processes start at once.
|
||||
const int memWarnThreshold = 100;
|
||||
const int memWarnThreshold = 250;
|
||||
|
||||
double prevTotalTime = 0.0;
|
||||
double prevRamUseMB = 0.0;
|
||||
struct rusage usage;
|
||||
static double prev_total_time = 0.0;
|
||||
struct mach_task_basic_info t_info;
|
||||
mach_msg_type_number_t t_info_count = MACH_TASK_BASIC_INFO_COUNT;
|
||||
struct mach_task_basic_info taskInfo;
|
||||
mach_msg_type_number_t taskInfoCount = MACH_TASK_BASIC_INFO_COUNT;
|
||||
|
||||
while(true) {
|
||||
sleep(timeInterval);
|
||||
@autoreleasepool {
|
||||
sleep(timeInterval);
|
||||
|
||||
// CPU
|
||||
getrusage(RUSAGE_SELF, &usage);
|
||||
double total_time = timeval_to_double(usage.ru_utime) + timeval_to_double(usage.ru_stime);
|
||||
double percentage = (((total_time - prev_total_time) / (double)timeInterval) * 100.0);
|
||||
prev_total_time = total_time;
|
||||
// CPU
|
||||
getrusage(RUSAGE_SELF, &usage);
|
||||
double totalTime = timeval_to_double(usage.ru_utime) + timeval_to_double(usage.ru_stime);
|
||||
double percentage = (((totalTime - prevTotalTime) / (double)timeInterval) * 100.0);
|
||||
prevTotalTime = totalTime;
|
||||
|
||||
if (percentage > cpuWarnThreshold) {
|
||||
LOGW(@"Watchdog: potentially high CPU use, ~%.2f%% over last %d seconds.",
|
||||
percentage, timeInterval);
|
||||
}
|
||||
if (percentage > cpuWarnThreshold) {
|
||||
LOGW(@"Watchdog: potentially high CPU use, ~%.2f%% over last %d seconds.",
|
||||
percentage, timeInterval);
|
||||
}
|
||||
|
||||
// RAM
|
||||
if (KERN_SUCCESS == task_info(mach_task_self(), MACH_TASK_BASIC_INFO,
|
||||
(task_info_t)&t_info, &t_info_count)) {
|
||||
double ramUseMb = (double) t_info.resident_size / 1024 / 1024;
|
||||
if (ramUseMb > (double)memWarnThreshold) {
|
||||
LOGW(@"Watchdog: potentially high RAM use, RSS is %.2fMB.", ramUseMb);
|
||||
// RAM
|
||||
if (KERN_SUCCESS == task_info(mach_task_self(), MACH_TASK_BASIC_INFO,
|
||||
(task_info_t)&taskInfo, &taskInfoCount)) {
|
||||
double ramUseMB = (double) taskInfo.resident_size / 1024 / 1024;
|
||||
if (ramUseMB > memWarnThreshold && ramUseMB > prevRamUseMB) {
|
||||
LOGW(@"Watchdog: potentially high RAM use, RSS is %.2fMB.", ramUseMB);
|
||||
}
|
||||
prevRamUseMB = ramUseMB;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,8 +77,8 @@ void *watchdog_thread_f(__unused void *idata) {
|
||||
|
||||
int main(int argc, const char *argv[]) {
|
||||
@autoreleasepool {
|
||||
// Do not buffer stdout
|
||||
setbuf(stdout, NULL);
|
||||
// Do not wait on child processes
|
||||
signal(SIGCHLD, SIG_IGN);
|
||||
|
||||
NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary];
|
||||
|
||||
@@ -83,14 +87,18 @@ int main(int argc, const char *argv[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Close stdout/stderr so logging goes to syslog
|
||||
fclose(stdout);
|
||||
fclose(stderr);
|
||||
|
||||
LOGI(@"Started, version %@", infoDict[@"CFBundleVersion"]);
|
||||
|
||||
SNTApplication *s = [[SNTApplication alloc] init];
|
||||
[s performSelectorInBackground:@selector(run) withObject:nil];
|
||||
|
||||
// Create watchdog thread
|
||||
pthread_t watchdog_thread;
|
||||
pthread_create(&watchdog_thread, NULL, watchdog_thread_f, NULL);
|
||||
pthread_t watchdogThread;
|
||||
pthread_create(&watchdogThread, NULL, watchdogThreadFunction, NULL);
|
||||
|
||||
[[NSRunLoop mainRunLoop] run];
|
||||
}
|
||||
|
||||
@@ -29,23 +29,30 @@
|
||||
///
|
||||
|
||||
#define TSTART(testName) \
|
||||
printf(" %-50s ", testName);
|
||||
do { printf(" %-50s ", testName); } while (0)
|
||||
#define TPASS() \
|
||||
printf("\x1b[32mPASS\x1b[0m\n");
|
||||
do { printf("\x1b[32mPASS\x1b[0m\n"); } while (0)
|
||||
#define TPASSINFO(fmt, ...) \
|
||||
printf("\x1b[32mPASS\x1b[0m\n " fmt "\n", ##__VA_ARGS__);
|
||||
do { printf("\x1b[32mPASS\x1b[0m\n " fmt "\n", ##__VA_ARGS__); } while (0)
|
||||
#define TFAIL() \
|
||||
printf("\x1b[31mFAIL\x1b[0m\n"); \
|
||||
exit(1);
|
||||
do { \
|
||||
printf("\x1b[31mFAIL\x1b[0m\n"); \
|
||||
exit(1); \
|
||||
} while (0)
|
||||
#define TFAILINFO(fmt, ...) \
|
||||
printf("\x1b[31mFAIL\x1b[0m\n -> " fmt "\n\nTest failed.\n\n", ##__VA_ARGS__); \
|
||||
exit(1);
|
||||
do { \
|
||||
printf("\x1b[31mFAIL\x1b[0m\n -> " fmt "\n\nTest failed.\n\n", ##__VA_ARGS__); \
|
||||
exit(1); \
|
||||
} while (0)
|
||||
|
||||
@interface SantaKernelTests : NSObject
|
||||
@property io_connect_t connection;
|
||||
@property int timesSeenLs;
|
||||
@property int timesSeenCat;
|
||||
@property int timesSeenCp;
|
||||
|
||||
@property int testExeIteration;
|
||||
@property int timesSeenTestExeIteration;
|
||||
- (void)runTests;
|
||||
@end
|
||||
|
||||
@@ -60,16 +67,15 @@
|
||||
t.standardInput = nil;
|
||||
t.standardOutput = nil;
|
||||
t.standardError = nil;
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
- (NSString *)sha256ForPath:(NSString *)path {
|
||||
unsigned char sha256[CC_SHA256_DIGEST_LENGTH];
|
||||
NSData *psData = [NSData dataWithContentsOfFile:path
|
||||
options:NSDataReadingMappedIfSafe
|
||||
error:nil];
|
||||
CC_SHA256([psData bytes], (unsigned int)[psData length], sha256);
|
||||
NSData *fData = [NSData dataWithContentsOfFile:path
|
||||
options:NSDataReadingMappedIfSafe
|
||||
error:nil];
|
||||
CC_SHA256([fData bytes], (unsigned int)[fData length], sha256);
|
||||
char buf[CC_SHA256_DIGEST_LENGTH * 2 + 1];
|
||||
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
|
||||
snprintf(buf + (2*i), 4, "%02x", (unsigned char)sha256[i]);
|
||||
@@ -80,8 +86,8 @@
|
||||
|
||||
#pragma mark - Driver Helpers
|
||||
|
||||
/// Call in-kernel function: |kSantaUserClientReceive| passing the |action| and |vnodeId| via a
|
||||
/// |santa_message_t| struct.
|
||||
/// Call in-kernel function: |kSantaUserClientAllowBinary| or |kSantaUserClientDenyBinary|
|
||||
/// passing the |vnodeID|.
|
||||
- (void)postToKernelAction:(santa_action_t)action forVnodeID:(uint64_t)vnodeid {
|
||||
if (action == ACTION_RESPOND_CHECKBW_ALLOW) {
|
||||
IOConnectCallScalarMethod(self.connection, kSantaUserClientAllowBinary, &vnodeid, 1, 0, 0);
|
||||
@@ -188,8 +194,15 @@
|
||||
}
|
||||
TPASS();
|
||||
|
||||
// Fetch the SHA-256 of /bin/ps, as we'll be using that for the cache invalidation test.
|
||||
NSString *psSHA = [self sha256ForPath:@"/bin/ps"];
|
||||
// Fetch the SHA-256 of /bin/ed, as we'll be using that for the cache invalidation test.
|
||||
NSString *edSHA = [self sha256ForPath:@"/bin/ed"];
|
||||
|
||||
// Create the RE used for matching testexe's
|
||||
NSString *cwd = [[NSFileManager defaultManager] currentDirectoryPath];
|
||||
NSString *pattern = [cwd stringByAppendingPathComponent:@"testexe\\.(\\d+)"];
|
||||
NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:pattern
|
||||
options:0
|
||||
error:NULL];
|
||||
|
||||
/// Begin listening for events
|
||||
queueMemory = (IODataQueueMemory *)address;
|
||||
@@ -198,7 +211,7 @@
|
||||
dataSize = sizeof(vdata);
|
||||
kr = IODataQueueDequeue(queueMemory, &vdata, &dataSize);
|
||||
if (kr == kIOReturnSuccess) {
|
||||
if ([[self sha256ForPath:@(vdata.path)] isEqual:psSHA]) {
|
||||
if ([[self sha256ForPath:@(vdata.path)] isEqual:edSHA]) {
|
||||
[self postToKernelAction:ACTION_RESPOND_CHECKBW_DENY forVnodeID:vdata.vnode_id];
|
||||
} else if (strncmp("/bin/mv", vdata.path, strlen("/bin/mv")) == 0) {
|
||||
[self postToKernelAction:ACTION_RESPOND_CHECKBW_DENY forVnodeID:vdata.vnode_id];
|
||||
@@ -220,6 +233,26 @@
|
||||
}
|
||||
TPASSINFO("Received pid, ppid: %d, %d", vdata.pid, vdata.ppid);
|
||||
} else {
|
||||
NSString *path = @(vdata.path);
|
||||
|
||||
// If current executable is one of our test exe's from handlesLotsOfBinaries,
|
||||
// check that the number has increased.
|
||||
NSArray *matches = [re matchesInString:path
|
||||
options:0
|
||||
range:NSMakeRange(0, path.length)];
|
||||
if (matches.count == 1 && [matches[0] numberOfRanges] == 2) {
|
||||
NSUInteger count = [[path substringWithRange:[matches[0] rangeAtIndex:1]] intValue];
|
||||
if (count <= self.testExeIteration && count > 0) {
|
||||
self.timesSeenTestExeIteration++;
|
||||
if (self.timesSeenTestExeIteration > 2) {
|
||||
TFAILINFO("Saw same binary several times");
|
||||
}
|
||||
} else {
|
||||
self.timesSeenTestExeIteration = 0;
|
||||
self.testExeIteration = (int)count;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow everything not related to our testing.
|
||||
[self postToKernelAction:ACTION_RESPOND_CHECKBW_ALLOW forVnodeID:vdata.vnode_id];
|
||||
}
|
||||
@@ -239,11 +272,11 @@
|
||||
- (void)receiveAndBlockTests {
|
||||
TSTART("Blocks denied binaries");
|
||||
|
||||
NSTask *ps = [self taskWithPath:@"/bin/ps"];
|
||||
NSTask *ed = [self taskWithPath:@"/bin/ed"];
|
||||
|
||||
@try {
|
||||
[ps launch];
|
||||
[ps waitUntilExit];
|
||||
[ed launch];
|
||||
[ed waitUntilExit];
|
||||
TFAIL();
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
@@ -282,12 +315,12 @@
|
||||
|
||||
// Copy the ls binary to a new file
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
if (![fm copyItemAtPath:@"/bin/pwd" toPath:@"santakerneltests_tmp" error:nil]) {
|
||||
if (![fm copyItemAtPath:@"/bin/pwd" toPath:@"invalidacachetest_tmp" error:nil]) {
|
||||
TFAILINFO("Failed to create temp file");
|
||||
}
|
||||
|
||||
// Launch the new file to put it in the cache
|
||||
NSTask *pwd = [self taskWithPath:@"santakerneltests_tmp"];
|
||||
NSTask *pwd = [self taskWithPath:@"invalidacachetest_tmp"];
|
||||
[pwd launch];
|
||||
[pwd waitUntilExit];
|
||||
|
||||
@@ -296,28 +329,42 @@
|
||||
TFAILINFO("First launch of test binary failed");
|
||||
}
|
||||
|
||||
// Now replace the contents of the test file (which is cached) with the contents of /bin/ps,
|
||||
// Now replace the contents of the test file (which is cached) with the contents of /bin/ed,
|
||||
// which is 'blacklisted' by SHA-256 during the tests.
|
||||
FILE *infile = fopen("/bin/ps", "r");
|
||||
FILE *outfile = fopen("santakerneltests_tmp", "w");
|
||||
FILE *infile = fopen("/bin/ed", "r");
|
||||
FILE *outfile = fopen("invalidacachetest_tmp", "w");
|
||||
int ch;
|
||||
while ((ch = fgetc(infile)) != EOF) {
|
||||
fputc(ch, outfile);
|
||||
}
|
||||
fclose(infile);
|
||||
fclose(outfile);
|
||||
|
||||
// Now try running the temp file again. If it succeeds, the test failed.
|
||||
NSTask *ps = [self taskWithPath:@"santakerneltests_tmp"];
|
||||
NSTask *ed = [self taskWithPath:@"invalidacachetest_tmp"];
|
||||
|
||||
@try {
|
||||
[ps launch];
|
||||
[ps waitUntilExit];
|
||||
TFAIL();
|
||||
[ed launch];
|
||||
[ed waitUntilExit];
|
||||
TFAILINFO("Launched after write while file open");
|
||||
[fm removeItemAtPath:@"invalidacachetest_tmp" error:nil];
|
||||
} @catch (NSException *exception) {
|
||||
// This is a pass, but we have more to do.
|
||||
}
|
||||
|
||||
// Close the file to flush the write.
|
||||
fclose(outfile);
|
||||
|
||||
// And try running the temp file again. If it succeeds, the test failed.
|
||||
ed = [self taskWithPath:@"invalidacachetest_tmp"];
|
||||
|
||||
@try {
|
||||
[ed launch];
|
||||
[ed waitUntilExit];
|
||||
TFAILINFO("Launched after file closed");
|
||||
} @catch (NSException *exception) {
|
||||
TPASS();
|
||||
} @finally {
|
||||
[fm removeItemAtPath:@"santakerneltests_tmp" error:nil];
|
||||
[fm removeItemAtPath:@"invalidacachetest_tmp" error:nil];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,12 +405,12 @@
|
||||
} else if (pid > 0) {
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
if (WIFEXITED(status) && WEXITSTATUS(status) == EACCES) {
|
||||
if (WIFEXITED(status) && WEXITSTATUS(status) == EPERM) {
|
||||
TPASS();
|
||||
} else if (WIFSTOPPED(status)) {
|
||||
TFAILINFO("Process was executed and is waiting for debugger");
|
||||
} else {
|
||||
TFAILINFO("Process did not exit with EACCESS as expected");
|
||||
TFAILINFO("Process did not exit with EPERM as expected");
|
||||
}
|
||||
} else if (pid == 0) {
|
||||
fclose(stdout);
|
||||
@@ -374,6 +421,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that the kernel can handle _lots_ of executions.
|
||||
- (void)handlesLotsOfBinaries {
|
||||
TSTART("Handles lots of binaries");
|
||||
|
||||
const int LIMIT = 12000;
|
||||
|
||||
for (int i = 0; i < LIMIT; i++) {
|
||||
printf("\033[s"); // save cursor position
|
||||
|
||||
printf("%d/%i", i+1, LIMIT);
|
||||
|
||||
NSString *fname = [@"testexe" stringByAppendingFormat:@".%i", i];
|
||||
[[NSFileManager defaultManager] copyItemAtPath:@"/bin/hostname" toPath:fname error:NULL];
|
||||
|
||||
@try {
|
||||
NSTask *testexec = [self taskWithPath:fname];
|
||||
[testexec launch];
|
||||
[testexec waitUntilExit];
|
||||
} @catch (NSException *e) {
|
||||
TFAILINFO("Failed to launch");
|
||||
}
|
||||
|
||||
unlink([fname UTF8String]);
|
||||
printf("\033[u"); // restore cursor position
|
||||
}
|
||||
printf("\033[K\033[u"); // clear line, restore cursor position
|
||||
|
||||
TPASS();
|
||||
}
|
||||
|
||||
#pragma mark - Main
|
||||
|
||||
- (void)runTests {
|
||||
@@ -388,7 +465,7 @@
|
||||
[self performSelectorInBackground:@selector(beginListening) withObject:nil];
|
||||
|
||||
// Wait for driver to finish getting ready
|
||||
sleep(1.0);
|
||||
sleep(1);
|
||||
printf("\n-> Functional tests:\033[m\n");
|
||||
|
||||
[self receiveAndBlockTests];
|
||||
@@ -396,6 +473,7 @@
|
||||
[self invalidatesCacheTests];
|
||||
[self clearCacheTests];
|
||||
[self blocksDeniedTracedBinaries];
|
||||
[self handlesLotsOfBinaries];
|
||||
|
||||
printf("\nAll tests passed.\n\n");
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@
|
||||
|
||||
self.mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMStub([self.mockFileInfo alloc]).andReturn(self.mockFileInfo);
|
||||
OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY]).andReturn(self.mockFileInfo);
|
||||
OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY
|
||||
error:[OCMArg setTo:nil]]).andReturn(self.mockFileInfo);
|
||||
|
||||
self.mockRuleDatabase = OCMClassMock([SNTRuleTable class]);
|
||||
self.mockEventDatabase = OCMClassMock([SNTEventTable class]);
|
||||
@@ -72,6 +73,16 @@
|
||||
notifierConnection:nil];
|
||||
}
|
||||
|
||||
/// Return a pre-configured santa_message_ t for testing with.
|
||||
- (santa_message_t)getMessage {
|
||||
santa_message_t message = {0};
|
||||
message.pid = 12;
|
||||
message.ppid = 1;
|
||||
message.vnode_id = 1234;
|
||||
strncpy(message.path, "/a/file", 7);
|
||||
return message;
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockFileInfo stopMocking];
|
||||
[self.mockCodesignChecker stopMocking];
|
||||
@@ -92,11 +103,7 @@
|
||||
rule.state = RULESTATE_WHITELIST;
|
||||
OCMExpect([self.mockRuleDatabase binaryRuleForSHA256:@"a"]).andReturn(rule);
|
||||
|
||||
[self.sut validateBinaryWithPath:@"/a/file"
|
||||
userName:@"nobody"
|
||||
pid:@(12)
|
||||
ppid:@(1)
|
||||
vnodeId:1234];
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_CHECKBW_ALLOW
|
||||
forVnodeID:1234]);
|
||||
@@ -112,11 +119,7 @@
|
||||
rule.state = RULESTATE_BLACKLIST;
|
||||
OCMExpect([self.mockRuleDatabase binaryRuleForSHA256:@"a"]).andReturn(rule);
|
||||
|
||||
[self.sut validateBinaryWithPath:@"/a/file"
|
||||
userName:@"nobody"
|
||||
pid:@(12)
|
||||
ppid:@(1)
|
||||
vnodeId:1234];
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_CHECKBW_DENY
|
||||
forVnodeID:1234]);
|
||||
@@ -134,11 +137,7 @@
|
||||
rule.state = RULESTATE_WHITELIST;
|
||||
OCMExpect([self.mockRuleDatabase certificateRuleForSHA256:@"a"]).andReturn(rule);
|
||||
|
||||
[self.sut validateBinaryWithPath:@"/a/file"
|
||||
userName:@"nobody"
|
||||
pid:@(12)
|
||||
ppid:@(1)
|
||||
vnodeId:1234];
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_CHECKBW_ALLOW
|
||||
forVnodeID:1234]);
|
||||
@@ -148,6 +147,8 @@
|
||||
id mockSut = OCMPartialMock(self.sut);
|
||||
OCMStub([mockSut fileIsInScope:OCMOCK_ANY]).andReturn(YES);
|
||||
|
||||
OCMExpect([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
|
||||
id cert = OCMClassMock([SNTCertificate class]);
|
||||
OCMExpect([self.mockCodesignChecker leafCertificate]).andReturn(cert);
|
||||
OCMExpect([cert SHA256]).andReturn(@"a");
|
||||
@@ -156,11 +157,7 @@
|
||||
rule.state = RULESTATE_BLACKLIST;
|
||||
OCMExpect([self.mockRuleDatabase certificateRuleForSHA256:@"a"]).andReturn(rule);
|
||||
|
||||
[self.sut validateBinaryWithPath:@"/a/file"
|
||||
userName:@"nobody"
|
||||
pid:@(12)
|
||||
ppid:@(1)
|
||||
vnodeId:1234];
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_CHECKBW_DENY
|
||||
forVnodeID:1234]);
|
||||
@@ -170,21 +167,15 @@
|
||||
id mockSut = OCMPartialMock(self.sut);
|
||||
OCMStub([mockSut fileIsInScope:OCMOCK_ANY]).andReturn(YES);
|
||||
|
||||
OCMExpect([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
OCMExpect([self.mockConfigurator clientMode]).andReturn(CLIENTMODE_MONITOR);
|
||||
[self.sut validateBinaryWithPath:@"/a/file"
|
||||
userName:@"nobody"
|
||||
pid:@(12)
|
||||
ppid:@(1)
|
||||
vnodeId:1234];
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_CHECKBW_ALLOW
|
||||
forVnodeID:1234]);
|
||||
|
||||
OCMExpect([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
OCMExpect([self.mockConfigurator clientMode]).andReturn(CLIENTMODE_LOCKDOWN);
|
||||
[self.sut validateBinaryWithPath:@"/a/file"
|
||||
userName:@"nobody"
|
||||
pid:@(12)
|
||||
ppid:@(1)
|
||||
vnodeId:1234];
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_CHECKBW_DENY
|
||||
forVnodeID:1234]);
|
||||
}
|
||||
@@ -194,13 +185,16 @@
|
||||
OCMStub([mockSut fileIsInScope:OCMOCK_ANY]).andReturn(NO);
|
||||
|
||||
OCMExpect([self.mockConfigurator clientMode]).andReturn(CLIENTMODE_LOCKDOWN);
|
||||
[self.sut validateBinaryWithPath:@"/a/file"
|
||||
userName:@"nobody"
|
||||
pid:@(24)
|
||||
ppid:@(1)
|
||||
vnodeId:1234];
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_CHECKBW_ALLOW
|
||||
forVnodeID:1234]);
|
||||
}
|
||||
|
||||
- (void)testMissingShasum {
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_CHECKBW_ALLOW
|
||||
forVnodeID:1234]);
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user