mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b0994a990 | ||
|
|
7dd616e891 | ||
|
|
c672edbe4d | ||
|
|
687ecc7097 | ||
|
|
b8882b4826 | ||
|
|
51de0b38a4 | ||
|
|
e0309c0482 | ||
|
|
5dbe86869d | ||
|
|
14a11279c7 | ||
|
|
df0ce42377 | ||
|
|
4c03411405 | ||
|
|
f020e18238 | ||
|
|
629bd4aff9 | ||
|
|
f20825a66c | ||
|
|
f098ca0d02 | ||
|
|
1f96f74f4d | ||
|
|
7a3a98c27a | ||
|
|
1130448cb9 | ||
|
|
d388e99c0e | ||
|
|
2baea9a6b4 | ||
|
|
0629625a9a | ||
|
|
a2d0acc761 | ||
|
|
28a6bce90f | ||
|
|
9058192ffe | ||
|
|
465b358271 | ||
|
|
7de585fe1d | ||
|
|
8479730c95 | ||
|
|
7102e2df4c | ||
|
|
c3bd99ff93 | ||
|
|
c560405a46 | ||
|
|
0c0fb28ccc | ||
|
|
a33fce942c | ||
|
|
369cd40ee5 | ||
|
|
577b431a41 | ||
|
|
75cf8acd33 | ||
|
|
d70983962b | ||
|
|
ff440984b0 | ||
|
|
c631155be7 | ||
|
|
6038930755 | ||
|
|
9edc119c62 | ||
|
|
269a94bf03 | ||
|
|
7f3e4d7468 | ||
|
|
eb89891cdd | ||
|
|
038b068370 | ||
|
|
d2017a59de | ||
|
|
3435b56a84 | ||
|
|
a812558d2d | ||
|
|
aefd85455e | ||
|
|
e42f1347b7 | ||
|
|
c7442a03d1 | ||
|
|
1eda8bdd9d | ||
|
|
c4d0628bdb | ||
|
|
d51ae66242 | ||
|
|
121dde6b8b | ||
|
|
98081b067d | ||
|
|
8cc9345b42 | ||
|
|
f7528365b0 | ||
|
|
7baa1a345e | ||
|
|
acf7f4fd52 | ||
|
|
f43e8680b8 | ||
|
|
545a6c1b36 | ||
|
|
f01fd8c850 | ||
|
|
c9ec69b0b5 | ||
|
|
3640e2c5f0 | ||
|
|
b3659cb456 | ||
|
|
76284a2916 | ||
|
|
40b1e011bd | ||
|
|
e0bebecd59 | ||
|
|
8ac0cf6831 | ||
|
|
992163206d | ||
|
|
86dd5d8078 | ||
|
|
932aa9d052 | ||
|
|
5f7f5204ec | ||
|
|
a154d23637 | ||
|
|
ac2bb9d362 | ||
|
|
b918958bfa | ||
|
|
215df4ffa6 | ||
|
|
bb28bc5875 | ||
|
|
a82bc3f712 | ||
|
|
b3a507014b | ||
|
|
49c5e35a14 | ||
|
|
869ed33bd4 | ||
|
|
0c4a9be482 | ||
|
|
4410ec575a | ||
|
|
e3b92fc948 | ||
|
|
4ca4692a67 | ||
|
|
c1284d3c23 | ||
|
|
c8c0eadf72 | ||
|
|
f4bbc8abc7 | ||
|
|
a0f6ea57f8 | ||
|
|
88d21a07ac | ||
|
|
88e3a606a0 | ||
|
|
fff693c3f0 | ||
|
|
1e8d792d39 | ||
|
|
dfb149ac6a | ||
|
|
b5cfc92261 | ||
|
|
079f3e3868 | ||
|
|
15a6d58785 | ||
|
|
a404498f8a | ||
|
|
0d133e2df6 | ||
|
|
488b28bfd5 | ||
|
|
0fceb7b2e1 | ||
|
|
a79d1a98e7 | ||
|
|
43434fd445 | ||
|
|
492e523884 | ||
|
|
3d1fdb7a2b | ||
|
|
95a4bf0ec7 | ||
|
|
0d4f261e14 | ||
|
|
e96288b41b | ||
|
|
deda1abcf7 | ||
|
|
ee79d75483 | ||
|
|
0e9e445ddf | ||
|
|
e64720bcd9 | ||
|
|
6e27590b57 | ||
|
|
916c3c7a2a | ||
|
|
8a5fde8ceb | ||
|
|
f5bd9bde7f | ||
|
|
b987f61924 | ||
|
|
482b51a2f9 | ||
|
|
93f2078eda | ||
|
|
158ae11e61 | ||
|
|
d282388266 | ||
|
|
6ecdfcba38 | ||
|
|
88dc8a547e | ||
|
|
58e24b3c11 | ||
|
|
5f1b3a2284 | ||
|
|
31be2584f2 | ||
|
|
a2311e5128 | ||
|
|
e94d42187b | ||
|
|
2b99cc3f62 | ||
|
|
cb7f782893 | ||
|
|
d5a0f8a74b | ||
|
|
2ebd71df24 | ||
|
|
479203f47c | ||
|
|
022b9209d9 | ||
|
|
771c2c868f | ||
|
|
5285a728b1 | ||
|
|
41e6583920 | ||
|
|
cbb60b3a05 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
.DS_Store
|
||||
Build
|
||||
Dist
|
||||
santa-*
|
||||
Pods
|
||||
Santa.xcodeproj/xcuserdata
|
||||
Santa.xcodeproj/project.xcworkspace
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
language: objective-c
|
||||
cache: cocoapods
|
||||
sudo: false
|
||||
osx_image: xcode7
|
||||
|
||||
before_install:
|
||||
- gem install activesupport
|
||||
- gem install cocoapods xcpretty
|
||||
- pod setup >/dev/null
|
||||
|
||||
script:
|
||||
- xcodebuild -workspace Santa.xcworkspace -scheme All -derivedDataPath build build test CODE_SIGN_IDENTITY='' | xcpretty -sc && exit ${PIPESTATUS[0]}
|
||||
|
||||
@@ -29,8 +29,8 @@ rake tests:kernel # only necessary if you're changing the kext code
|
||||
|
||||
All code submissions should try to match the surrounding code. Wherever possible,
|
||||
code should adhere to either the
|
||||
[Google Objective-C Style Guide](http://google-styleguide.googlecode.com/svn/trunk/objcguide.xml)
|
||||
or the [Google C++ Style Guide](http://google-styleguide.googlecode.com/svn/trunk/cppguide.html).
|
||||
[Google Objective-C Style Guide](https://google.github.io/styleguide/objcguide.xml)
|
||||
or the [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html).
|
||||
|
||||
### The small print
|
||||
Contributions made by corporations are covered by a different agreement than
|
||||
|
||||
@@ -2,7 +2,5 @@
|
||||
> /var/log/santa.log format="[$((Time)(ISO8601Z.3))] $Message" mode=0644 rotate=seq compress file_max=25M all_max=100M uid=0 gid=0
|
||||
? [= Sender kernel] [S= Message santa-driver:] claim
|
||||
? [= Sender kernel] [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
|
||||
? [= Facility com.google.santa] claim
|
||||
? [= Facility com.google.santa] file /var/log/santa.log
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Library/Extensions/santa-driver.kext/Contents/MacOS/santad</string>
|
||||
<string>--syslog</string>
|
||||
</array>
|
||||
<key>MachServices</key>
|
||||
<dict>
|
||||
@@ -19,5 +20,7 @@
|
||||
<true />
|
||||
<key>ProcessType</key>
|
||||
<string>Interactive</string>
|
||||
<key>ThrottleInterval</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Applications/Santa.app/Contents/MacOS/Santa</string>
|
||||
<string>--syslog</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
42
Podfile
42
Podfile
@@ -7,34 +7,42 @@ target :Santa do
|
||||
pod 'MOLCodesignChecker'
|
||||
end
|
||||
|
||||
target :santactl do
|
||||
target :santad do
|
||||
pod 'FMDB'
|
||||
pod 'MOLCertificate'
|
||||
pod 'MOLCodesignChecker'
|
||||
pod 'FMDB'
|
||||
end
|
||||
|
||||
target :santad do
|
||||
target :santactl do
|
||||
pod 'FMDB'
|
||||
pod 'MOLAuthenticatingURLSession'
|
||||
pod 'MOLCertificate'
|
||||
pod 'MOLCodesignChecker'
|
||||
pod 'FMDB'
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
target :LogicTests do
|
||||
pod 'FMDB'
|
||||
pod 'MOLAuthenticatingURLSession'
|
||||
pod 'MOLCertificate'
|
||||
pod 'MOLCodesignChecker'
|
||||
pod 'OCMock'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
if config.name != 'Release' then
|
||||
break
|
||||
end
|
||||
|
||||
# This is necessary to get FMDB to not NSLog stuff.
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ''
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] <<= "NDEBUG=1"
|
||||
|
||||
# Enable more compiler optimizations.
|
||||
config.build_settings['GCC_OPTIMIZATION_LEVEL'] = 'fast'
|
||||
config.build_settings['LLVM_LTO'] = 'YES'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
28
Podfile.lock
28
Podfile.lock
@@ -1,22 +1,28 @@
|
||||
PODS:
|
||||
- FMDB (2.6):
|
||||
- FMDB/standard (= 2.6)
|
||||
- FMDB/standard (2.6)
|
||||
- MOLCertificate (1.3)
|
||||
- MOLCodesignChecker (1.4):
|
||||
- FMDB (2.6.2):
|
||||
- FMDB/standard (= 2.6.2)
|
||||
- FMDB/standard (2.6.2)
|
||||
- MOLAuthenticatingURLSession (1.6):
|
||||
- MOLCertificate (~> 1.3)
|
||||
- OCMock (3.2.2)
|
||||
- MOLCertificate (1.5)
|
||||
- MOLCodesignChecker (1.5):
|
||||
- MOLCertificate (~> 1.3)
|
||||
- OCMock (3.3)
|
||||
|
||||
DEPENDENCIES:
|
||||
- FMDB
|
||||
- MOLAuthenticatingURLSession
|
||||
- MOLCertificate
|
||||
- MOLCodesignChecker
|
||||
- OCMock
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
FMDB: c1968bab3ab0aed38f66cb778ae1e7fa9a652b6e
|
||||
MOLCertificate: a776221906b5a46dd1bd749d0682bef3ee68c1f5
|
||||
MOLCodesignChecker: 34e60cc6beadabfb4762b6e5087e12837774f85f
|
||||
OCMock: 18c9b7e67d4c2770e95bb77a9cc1ae0c91fe3835
|
||||
FMDB: 854a0341b4726e53276f2a8996f06f1b80f9259a
|
||||
MOLAuthenticatingURLSession: f956240458fb24b61e5607d735948dc9babfb4e3
|
||||
MOLCertificate: c39cae866d24d36fbc78032affff83d401b5384a
|
||||
MOLCodesignChecker: fc9c64147811d7b0d0739127003e0630dff9213a
|
||||
OCMock: d68685bde31f69cb61d518dcb39269080c78b5ed
|
||||
|
||||
COCOAPODS: 0.39.0
|
||||
PODFILE CHECKSUM: bc456d69693ca262c781dbbde40529a9474b84b5
|
||||
|
||||
COCOAPODS: 1.0.1
|
||||
|
||||
15
README.md
15
README.md
@@ -7,7 +7,7 @@ Santa [](h
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Santa is a binary whitelisting/blacklisting system for OS X. It consists of
|
||||
Santa is a binary whitelisting/blacklisting system for macOS. 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
|
||||
@@ -68,7 +68,7 @@ hosts in whatever other ways you see fit.
|
||||
Get Help
|
||||
========
|
||||
|
||||
If you have questions or need help getting started, the
|
||||
If you have questions or need help getting started, the
|
||||
[santa-dev](https://groups.google.com/forum/#!forum/santa-dev) group is the
|
||||
best place to start.
|
||||
|
||||
@@ -108,6 +108,15 @@ option) if it would be useful to others.
|
||||
|
||||
* Tests: There aren't enough of them.
|
||||
|
||||
Screenshots
|
||||
===========
|
||||
|
||||
A tool like Santa doesn't really lend itself to screenshots, so here's a video instead.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://zippy.gfycat.com/MadFatalAmphiuma.gif" alt="Santa Block Video" />
|
||||
</p>
|
||||
|
||||
Building
|
||||
========
|
||||
```sh
|
||||
@@ -127,7 +136,7 @@ and for security-reasons parts of Santa will not operate properly if not signed.
|
||||
|
||||
Kext Signing
|
||||
============
|
||||
Kernel extensions on OS X 10.9 and later must be signed using an Apple-provided
|
||||
Kernel extensions on macOS 10.9 and later must be signed using an Apple-provided
|
||||
Developer ID certificate with a kernel extension flag. Without it, the only way
|
||||
to load an extension is to enable kext-dev-mode or disable SIP, depending on the
|
||||
OS version.
|
||||
|
||||
20
Rakefile
20
Rakefile
@@ -1,7 +1,6 @@
|
||||
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'
|
||||
@@ -56,7 +55,6 @@ desc "Clean"
|
||||
task :clean => :init do
|
||||
puts "Cleaning"
|
||||
FileUtils.rm_rf(OUTPUT_PATH)
|
||||
FileUtils.rm_rf(DIST_PATH)
|
||||
end
|
||||
|
||||
# Build
|
||||
@@ -111,24 +109,26 @@ task :dist do
|
||||
Rake::Task['clean'].invoke()
|
||||
Rake::Task['build:build'].invoke("Release")
|
||||
|
||||
FileUtils.rm_rf(DIST_PATH)
|
||||
dist_path = "santa-#{`defaults read #{xcodebuilddir}/Release/santa-driver.kext/Contents/Info.plist CFBundleVersion`.strip}"
|
||||
|
||||
FileUtils.mkdir_p("#{DIST_PATH}/binaries")
|
||||
FileUtils.mkdir_p("#{DIST_PATH}/conf")
|
||||
FileUtils.mkdir_p("#{DIST_PATH}/dsym")
|
||||
FileUtils.rm_rf(dist_path)
|
||||
|
||||
FileUtils.mkdir_p("#{dist_path}/binaries")
|
||||
FileUtils.mkdir_p("#{dist_path}/conf")
|
||||
FileUtils.mkdir_p("#{dist_path}/dsym")
|
||||
|
||||
BINARIES.each do |x|
|
||||
FileUtils.cp_r("#{xcodebuilddir}/Release/#{x}", "#{DIST_PATH}/binaries")
|
||||
FileUtils.cp_r("#{xcodebuilddir}/Release/#{x}", "#{dist_path}/binaries")
|
||||
end
|
||||
|
||||
DSYMS.each do |x|
|
||||
FileUtils.cp_r("#{xcodebuilddir}/Release/#{x}", "#{DIST_PATH}/dsym")
|
||||
FileUtils.cp_r("#{xcodebuilddir}/Release/#{x}", "#{dist_path}/dsym")
|
||||
end
|
||||
|
||||
|
||||
Dir.glob("Conf/*") {|x| File.directory?(x) or FileUtils.cp(x, "#{DIST_PATH}/conf")}
|
||||
Dir.glob("Conf/*") {|x| File.directory?(x) or FileUtils.cp(x, "#{dist_path}/conf")}
|
||||
|
||||
puts "Distribution folder created"
|
||||
puts "Distribution folder #{dist_path} created"
|
||||
end
|
||||
|
||||
# Tests
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0700"
|
||||
LastUpgradeVersion = "0730"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,7 +26,9 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES"
|
||||
enableAddressSanitizer = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0700"
|
||||
LastUpgradeVersion = "0730"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0700"
|
||||
LastUpgradeVersion = "0730"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0700"
|
||||
LastUpgradeVersion = "0730"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0700"
|
||||
LastUpgradeVersion = "0730"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0700"
|
||||
LastUpgradeVersion = "0730"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0700"
|
||||
LastUpgradeVersion = "0730"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -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 OS X.
|
||||
<string key="title">Santa is an application whitelisting system for macOS.
|
||||
|
||||
There are no user-configurable settings.</string>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="8191" systemVersion="15C50" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="8191"/>
|
||||
<development version="6300" identifier="xcode"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10117"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="SNTMessageWindowController">
|
||||
@@ -14,64 +15,95 @@
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="none" id="9Bq-yh-54f" customClass="SNTMessageWindow">
|
||||
<window title="Santa Blocked Execution" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="none" id="9Bq-yh-54f" customClass="SNTMessageWindow">
|
||||
<windowStyleMask key="styleMask" utility="YES"/>
|
||||
<rect key="contentRect" x="167" y="107" width="497" height="381"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
|
||||
<rect key="contentRect" x="167" y="107" width="497" height="439"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
|
||||
<view key="contentView" id="Iwq-Lx-rLv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="497" height="381"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="497" height="439"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kiB-jZ-69S">
|
||||
<rect key="frame" x="-6" y="411" width="37" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Hidden Button" alternateTitle="This button exists so neither of the other two buttons is pre-selected when the dialog opens." bezelStyle="rounded" alignment="center" borderStyle="border" focusRingType="none" transparent="YES" imageScaling="proportionallyDown" inset="2" id="XGa-Sl-F4t">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<outlet property="nextKeyView" destination="7ua-5a-uSd" id="vl5-A8-O0H"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="t8c-Fx-e5h">
|
||||
<rect key="frame" x="207" y="311" width="83" height="40"/>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Santa" id="7YA-iB-Zma">
|
||||
<font key="font" size="34" name="HelveticaNeue-UltraLight"/>
|
||||
<color key="textColor" red="0.18696189413265307" green="0.18696189413265307" blue="0.18696189413265307" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<rect key="frame" x="206" y="368" width="85" height="41"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" refusesFirstResponder="YES" sendsActionOnEndEditing="YES" title="Santa" id="7YA-iB-Zma">
|
||||
<font key="font" metaFont="systemUltraLight" size="34"/>
|
||||
<color key="textColor" red="0.20000000000000001" green="0.20000000000000001" blue="0.20000000000000001" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<outlet property="nextKeyView" destination="7ua-5a-uSd" id="z5y-RR-IEH"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cD5-Su-lXR">
|
||||
<rect key="frame" x="22" y="264" width="454" height="17"/>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cD5-Su-lXR" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="22" y="329" width="454" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="450" id="XgJ-EV-tBa"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<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">
|
||||
<textFieldCell key="cell" selectable="YES" refusesFirstResponder="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="center" title="A message to the user goes here..." allowsEditingTextAttributes="YES" id="5tH-bG-UJA">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.attributedCustomMessage" id="376-sj-4Q1"/>
|
||||
<outlet property="nextKeyView" destination="7ua-5a-uSd" id="VC7-bE-uHc"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pc8-G9-4pJ">
|
||||
<rect key="frame" x="165" y="192" width="294" height="17"/>
|
||||
<box horizontalHuggingPriority="750" title="Line" boxType="custom" borderType="line" titlePosition="noTitle" translatesAutoresizingMaskIntoConstraints="NO" id="4Li-ul-zIi">
|
||||
<rect key="frame" x="146" y="132" width="1" height="167"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="290" id="xVR-j3-dLw"/>
|
||||
<constraint firstAttribute="width" constant="1" id="0o1-Jh-epf"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Binary Path" id="E7T-9h-ofr">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" white="0.0" alpha="0.5" colorSpace="deviceWhite"/>
|
||||
<color key="borderColor" white="0.0" alpha="0.17999999999999999" colorSpace="calibratedWhite"/>
|
||||
<color key="fillColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
<font key="titleFont" metaFont="system"/>
|
||||
</box>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pDa-fA-vnC" userLabel="Label: Application">
|
||||
<rect key="frame" x="8" y="282" width="120" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="116" id="8mA-zi-Ev7"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Application" id="Hy7-WF-6xW">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.filePath" id="qfp-sR-Nmu"/>
|
||||
<binding destination="-2" name="hidden" keyPath="self.event.fileBundleName" id="r2Q-hh-Uy5">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSIsNil</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qgf-Jf-cJr">
|
||||
<rect key="frame" x="165" y="217" width="294" height="17"/>
|
||||
<textField toolTip="Application Name" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qgf-Jf-cJr" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="165" y="282" width="294" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="290" id="Pav-ZA-iAu"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Application Name" id="3UG-ca-d1k">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" white="0.0" alpha="0.5" colorSpace="deviceWhite"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="textColor" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.fileBundleName" id="enC-Cl-UWt">
|
||||
@@ -81,31 +113,68 @@
|
||||
</binding>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="PXc-xv-A28">
|
||||
<rect key="frame" x="165" y="142" width="219" height="17"/>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="d9e-Wv-Y5H" userLabel="Label: Path">
|
||||
<rect key="frame" x="8" y="257" width="120" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="215" id="4hh-R2-86s"/>
|
||||
<constraint firstAttribute="width" constant="116" id="Kqd-nX-7df"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" lineBreakMode="charWrapping" selectable="YES" sendsActionOnEndEditing="YES" title="SHA-256" id="X4W-9e-eIu">
|
||||
<font key="font" metaFont="fixedUser" size="11"/>
|
||||
<color key="textColor" white="0.0" alpha="0.5" colorSpace="deviceWhite"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Filename" id="KgY-X1-ESG">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YNz-ka-cBi" userLabel="Label: Path">
|
||||
<rect key="frame" x="8" y="232" width="120" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="116" id="3wU-P0-gAC"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Path" id="adC-be-Beh">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField toolTip="Binary Path" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pc8-G9-4pJ" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="165" y="257" width="294" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="290" id="xVR-j3-dLw"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Binary Name" id="E7T-9h-ofr">
|
||||
<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="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.fileSHA256" id="9KB-0b-qLV"/>
|
||||
<binding destination="-2" name="value" keyPath="self.event.filePath.lastPathComponent" id="bOu-gv-1Vh"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="C3G-wL-u7w">
|
||||
<rect key="frame" x="165" y="167" width="294" height="17"/>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="lvJ-Rk-UT5" userLabel="Label: Publisher">
|
||||
<rect key="frame" x="8" y="207" width="120" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Publisher" id="yL9-yD-JXX">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField toolTip="Publisher" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="C3G-wL-u7w" userLabel="Publisher" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="165" y="207" width="294" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="290" id="Dem-wH-KHm"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" selectable="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" title="Code signing information" placeholderString="" id="ztA-La-XgT">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" white="0.0" alpha="0.5" colorSpace="deviceWhite"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="textColor" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.publisherInfo" id="CEI-Cu-7pC">
|
||||
@@ -115,93 +184,18 @@
|
||||
</binding>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="oFj-ol-xpL">
|
||||
<rect key="frame" x="8" y="92" width="120" height="17"/>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="User" id="1ut-uT-hQD">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eQb-0a-76J">
|
||||
<rect key="frame" x="8" y="117" width="120" height="17"/>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Parent" id="gze-4A-1w5">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="lvJ-Rk-UT5">
|
||||
<rect key="frame" x="8" y="167" width="120" height="17"/>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Publisher" id="yL9-yD-JXX">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="d9e-Wv-Y5H">
|
||||
<rect key="frame" x="8" y="192" width="120" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="116" id="Kqd-nX-7df"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Path" id="KgY-X1-ESG">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KEB-eH-x2Y">
|
||||
<rect key="frame" x="8" y="142" width="120" height="17"/>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Identifier" id="eKN-Ic-5zy">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="h6f-PY-cc0">
|
||||
<rect key="frame" x="165" y="92" width="294" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="290" id="on6-pj-m2k"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<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"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.executingUser" id="IcM-Lt-xTT">
|
||||
<dictionary key="options">
|
||||
<string key="NSNullPlaceholder">Unknown</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</textField>
|
||||
<box horizontalHuggingPriority="750" title="Line" boxType="custom" borderType="line" titlePosition="noTitle" translatesAutoresizingMaskIntoConstraints="NO" id="4Li-ul-zIi">
|
||||
<rect key="frame" x="146" y="92" width="1" height="142"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="1" id="0o1-Jh-epf"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<color key="borderColor" white="0.0" alpha="0.17999999999999999" colorSpace="calibratedWhite"/>
|
||||
<color key="fillColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
<font key="titleFont" metaFont="system"/>
|
||||
</box>
|
||||
<button toolTip="Show code signing certificate chain" translatesAutoresizingMaskIntoConstraints="NO" id="cJf-k6-OxS" userLabel="Publisher Certs">
|
||||
<rect key="frame" x="40" y="168" width="15" height="15"/>
|
||||
<button toolTip="Show code signing certificate chain" translatesAutoresizingMaskIntoConstraints="NO" id="cJf-k6-OxS" userLabel="Publisher Certs Button">
|
||||
<rect key="frame" x="40" y="208" width="15" height="15"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="15" id="QTm-Iv-m5p"/>
|
||||
<constraint firstAttribute="height" constant="15" id="YwG-0s-jop"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="bevel" bezelStyle="regularSquare" image="NSInfo" imagePosition="overlaps" alignment="center" refusesFirstResponder="YES" imageScaling="proportionallyDown" inset="2" id="R72-Qy-Xbb">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="showCertInfo:" target="-2" id="dB0-a3-X31"/>
|
||||
@@ -212,55 +206,54 @@
|
||||
</binding>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="BbV-3h-mmL">
|
||||
<rect key="frame" x="256" y="33" width="110" height="25"/>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KEB-eH-x2Y" userLabel="Label: Identifier">
|
||||
<rect key="frame" x="8" y="182" width="120" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Identifier" id="eKN-Ic-5zy">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="PXc-xv-A28">
|
||||
<rect key="frame" x="165" y="182" width="219" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="110" id="6Uh-Bd-N64"/>
|
||||
<constraint firstAttribute="height" constant="22" id="GH6-nw-6rD"/>
|
||||
<constraint firstAttribute="width" constant="215" id="4hh-R2-86s"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="roundTextured" title="Dismiss" bezelStyle="texturedRounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="XR6-Xa-gP4">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES"/>
|
||||
</buttonCell>
|
||||
<textFieldCell key="cell" lineBreakMode="charWrapping" selectable="YES" sendsActionOnEndEditing="YES" title="SHA-256" id="X4W-9e-eIu">
|
||||
<font key="font" metaFont="fixedUser" size="11"/>
|
||||
<color key="textColor" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<action selector="closeWindow:" target="-2" id="qQq-gh-8lw"/>
|
||||
<outlet property="nextKeyView" destination="7ua-5a-uSd" id="4KL-Z2-1op"/>
|
||||
<binding destination="-2" name="value" keyPath="self.event.fileSHA256" id="9KB-0b-qLV"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="7ua-5a-uSd">
|
||||
<rect key="frame" x="132" y="33" width="112" height="25"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" priority="900" constant="112" id="Pec-Pa-4aZ"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="roundTextured" title="Open Event..." bezelStyle="texturedRounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="openEventDetails:" target="-2" id="VhL-ql-rCV"/>
|
||||
<outlet property="nextKeyView" destination="BbV-3h-mmL" id="Xkz-va-iGc"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="f1p-GL-O3o">
|
||||
<rect key="frame" x="165" y="117" width="294" height="17"/>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eQb-0a-76J" userLabel="Label: Parent">
|
||||
<rect key="frame" x="8" y="157" width="120" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Parent" id="gze-4A-1w5">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField toolTip="Parent Process" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="f1p-GL-O3o" userLabel="Parent" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="165" y="157" width="294" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="290" id="h3Y-mO-38F"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<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"/>
|
||||
<color key="textColor" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="displayPatternValue1" keyPath="self.event.parentName" id="Lce-TO-q9V">
|
||||
@@ -275,96 +268,163 @@ DQ
|
||||
</binding>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pDa-fA-vnC">
|
||||
<rect key="frame" x="8" y="217" width="120" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="116" id="8mA-zi-Ev7"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Application" id="Hy7-WF-6xW">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="oFj-ol-xpL" userLabel="Label: User">
|
||||
<rect key="frame" x="8" y="132" width="120" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="User" id="1ut-uT-hQD">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField toolTip="Executing User" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="h6f-PY-cc0" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="165" y="132" width="294" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="290" id="on6-pj-m2k"/>
|
||||
</constraints>
|
||||
<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" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="hidden" keyPath="self.event.fileBundleName" id="r2Q-hh-Uy5">
|
||||
<binding destination="-2" name="value" keyPath="self.event.executingUser" id="IcM-Lt-xTT">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSIsNil</string>
|
||||
<string key="NSNullPlaceholder">Unknown</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</textField>
|
||||
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kiB-jZ-69S">
|
||||
<rect key="frame" x="-6" y="353" width="37" height="32"/>
|
||||
<animations/>
|
||||
<buttonCell key="cell" type="push" title="Hidden Button" alternateTitle="This button exists so neither of the other two buttons is pre-selected when the dialog opens." bezelStyle="rounded" alignment="center" borderStyle="border" focusRingType="none" transparent="YES" imageScaling="proportionallyDown" inset="2" id="XGa-Sl-F4t">
|
||||
<button verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="7ua-5a-uSd">
|
||||
<rect key="frame" x="132" y="33" width="112" height="25"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" priority="900" constant="112" id="Pec-Pa-4aZ"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="roundTextured" title="Open Event..." bezelStyle="texturedRounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<outlet property="nextKeyView" destination="7ua-5a-uSd" id="vl5-A8-O0H"/>
|
||||
<action selector="openEventDetails:" target="-2" id="VhL-ql-rCV"/>
|
||||
<outlet property="nextKeyView" destination="BbV-3h-mmL" id="Xkz-va-iGc"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="BbV-3h-mmL" userLabel="Dismiss Button">
|
||||
<rect key="frame" x="256" y="33" width="110" height="25"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="110" id="6Uh-Bd-N64"/>
|
||||
<constraint firstAttribute="height" constant="22" id="GH6-nw-6rD"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="roundTextured" title="Dismiss" bezelStyle="texturedRounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="XR6-Xa-gP4">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES"/>
|
||||
</buttonCell>
|
||||
<accessibility description="Dismiss Dialog"/>
|
||||
<connections>
|
||||
<action selector="closeWindow:" target="-2" id="qQq-gh-8lw"/>
|
||||
<outlet property="nextKeyView" destination="7ua-5a-uSd" id="4KL-Z2-1op"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField toolTip="Binary Path" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bDE-Tl-UHg" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="165" y="232" width="294" height="17"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="290" id="p1W-f9-KBX"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Binary Path" id="H1b-Ui-CYo">
|
||||
<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="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.filePath" id="Sry-KY-HDb"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="5D8-GP-a4l">
|
||||
<rect key="frame" x="91" y="80" width="315" height="29"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="25" id="KvD-X6-CsO"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="check" title="Prevent future notifications for this application for a day" bezelStyle="regularSquare" imagePosition="left" alignment="center" inset="2" id="R5Y-Uc-rEP">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.silenceFutureNotifications" id="tEb-2A-sht"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="oFj-ol-xpL" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="0AD-PS-5V1"/>
|
||||
<constraint firstItem="f1p-GL-O3o" firstAttribute="centerY" secondItem="eQb-0a-76J" secondAttribute="centerY" id="2Aq-1E-Ybz"/>
|
||||
<constraint firstItem="BbV-3h-mmL" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" priority="500" constant="193" id="2uo-Cm-Tfp"/>
|
||||
<constraint firstItem="h6f-PY-cc0" firstAttribute="top" secondItem="f1p-GL-O3o" secondAttribute="bottom" constant="8" id="496-VQ-Fx5"/>
|
||||
<constraint firstItem="eQb-0a-76J" firstAttribute="leading" secondItem="lvJ-Rk-UT5" secondAttribute="trailing" constant="-116" id="6Q5-Oo-1cI"/>
|
||||
<constraint firstItem="BbV-3h-mmL" firstAttribute="top" secondItem="oFj-ol-xpL" secondAttribute="bottom" constant="35" id="7K6-bY-Rn6"/>
|
||||
<constraint firstItem="pDa-fA-vnC" firstAttribute="centerY" secondItem="qgf-Jf-cJr" secondAttribute="centerY" id="AKX-pe-hEX"/>
|
||||
<constraint firstItem="C3G-wL-u7w" firstAttribute="leading" secondItem="4Li-ul-zIi" secondAttribute="trailing" constant="20" id="ALv-0v-szi"/>
|
||||
<constraint firstItem="f1p-GL-O3o" firstAttribute="top" secondItem="PXc-xv-A28" secondAttribute="bottom" constant="8" id="E6D-7P-17g"/>
|
||||
<constraint firstItem="d9e-Wv-Y5H" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="FPe-Rd-G4n"/>
|
||||
<constraint firstItem="cJf-k6-OxS" firstAttribute="centerY" secondItem="C3G-wL-u7w" secondAttribute="centerY" id="FdL-ZZ-Vbe"/>
|
||||
<constraint firstItem="t8c-Fx-e5h" firstAttribute="top" secondItem="Iwq-Lx-rLv" secondAttribute="top" constant="30" id="FuB-GX-0jg"/>
|
||||
<constraint firstItem="oFj-ol-xpL" firstAttribute="bottom" secondItem="4Li-ul-zIi" secondAttribute="bottom" id="G0I-O2-S91"/>
|
||||
<constraint firstItem="lvJ-Rk-UT5" firstAttribute="leading" secondItem="cJf-k6-OxS" secondAttribute="trailing" constant="-45" id="GD2-Ka-deo"/>
|
||||
<constraint firstItem="BbV-3h-mmL" firstAttribute="top" secondItem="5D8-GP-a4l" secondAttribute="bottom" priority="900" constant="25" id="GT2-tO-2td"/>
|
||||
<constraint firstItem="h6f-PY-cc0" firstAttribute="centerY" secondItem="oFj-ol-xpL" secondAttribute="centerY" id="GXI-pT-FM1"/>
|
||||
<constraint firstItem="4Li-ul-zIi" firstAttribute="top" secondItem="pDa-fA-vnC" secondAttribute="top" id="Gd4-Nr-n5G"/>
|
||||
<constraint firstItem="qgf-Jf-cJr" firstAttribute="leading" secondItem="4Li-ul-zIi" secondAttribute="trailing" constant="20" id="Ht4-Lg-U5N"/>
|
||||
<constraint firstItem="oFj-ol-xpL" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="IwX-ja-ZIs"/>
|
||||
<constraint firstItem="d9e-Wv-Y5H" firstAttribute="top" secondItem="4Li-ul-zIi" secondAttribute="top" priority="500" id="JY4-N1-j8e"/>
|
||||
<constraint firstItem="oFj-ol-xpL" firstAttribute="leading" secondItem="d9e-Wv-Y5H" secondAttribute="leading" priority="999" id="MVr-jY-GDj"/>
|
||||
<constraint firstItem="pc8-G9-4pJ" firstAttribute="top" secondItem="cD5-Su-lXR" secondAttribute="bottom" priority="750" constant="30" id="Nsl-zf-poH"/>
|
||||
<constraint firstItem="YNz-ka-cBi" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="KmX-kX-VCN"/>
|
||||
<constraint firstItem="5D8-GP-a4l" firstAttribute="centerX" secondItem="Iwq-Lx-rLv" secondAttribute="centerX" id="LkH-F4-Ncm"/>
|
||||
<constraint firstItem="pc8-G9-4pJ" firstAttribute="top" secondItem="cD5-Su-lXR" secondAttribute="bottom" priority="950" constant="30" id="Nsl-zf-poH"/>
|
||||
<constraint firstItem="YNz-ka-cBi" firstAttribute="centerY" secondItem="bDE-Tl-UHg" secondAttribute="centerY" id="ObQ-RA-S5V"/>
|
||||
<constraint firstItem="pc8-G9-4pJ" firstAttribute="leading" secondItem="4Li-ul-zIi" secondAttribute="trailing" constant="20" id="SCl-Ky-VmT"/>
|
||||
<constraint firstItem="d9e-Wv-Y5H" firstAttribute="centerY" secondItem="pc8-G9-4pJ" secondAttribute="centerY" id="SLv-F7-w5k"/>
|
||||
<constraint firstItem="7ua-5a-uSd" firstAttribute="top" secondItem="oFj-ol-xpL" secondAttribute="bottom" constant="35" id="Scq-zQ-Sao"/>
|
||||
<constraint firstItem="4Li-ul-zIi" firstAttribute="leading" secondItem="KEB-eH-x2Y" secondAttribute="trailing" constant="20" id="Seb-c0-MUL"/>
|
||||
<constraint firstAttribute="centerX" secondItem="cD5-Su-lXR" secondAttribute="centerX" id="V0a-Py-iEc"/>
|
||||
<constraint firstItem="oFj-ol-xpL" firstAttribute="leading" secondItem="lvJ-Rk-UT5" secondAttribute="leading" priority="999" id="Z6G-l9-G4a"/>
|
||||
<constraint firstItem="oFj-ol-xpL" firstAttribute="top" secondItem="eQb-0a-76J" secondAttribute="bottom" constant="8" id="abm-cM-PN0"/>
|
||||
<constraint firstItem="pDa-fA-vnC" firstAttribute="leading" secondItem="oFj-ol-xpL" secondAttribute="leading" id="asc-Ga-WHD"/>
|
||||
<constraint firstItem="C3G-wL-u7w" firstAttribute="top" secondItem="bDE-Tl-UHg" secondAttribute="bottom" constant="8" id="ZoS-xV-2WA"/>
|
||||
<constraint firstItem="lvJ-Rk-UT5" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="aMJ-Wb-vRS"/>
|
||||
<constraint firstItem="KEB-eH-x2Y" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="aOk-S0-0n2"/>
|
||||
<constraint firstItem="4Li-ul-zIi" firstAttribute="leading" secondItem="eQb-0a-76J" secondAttribute="trailing" constant="20" id="b0B-3w-grH"/>
|
||||
<constraint firstItem="KEB-eH-x2Y" firstAttribute="leading" secondItem="oFj-ol-xpL" secondAttribute="leading" priority="999" id="b5A-M7-ZsD"/>
|
||||
<constraint firstItem="KEB-eH-x2Y" firstAttribute="centerY" secondItem="PXc-xv-A28" secondAttribute="centerY" id="cHe-pZ-0Oq"/>
|
||||
<constraint firstItem="cD5-Su-lXR" firstAttribute="top" secondItem="t8c-Fx-e5h" secondAttribute="bottom" constant="30" id="dYg-zP-wh2"/>
|
||||
<constraint firstItem="cD5-Su-lXR" firstAttribute="top" secondItem="t8c-Fx-e5h" secondAttribute="bottom" constant="22" id="dYg-zP-wh2"/>
|
||||
<constraint firstItem="h6f-PY-cc0" firstAttribute="leading" secondItem="4Li-ul-zIi" secondAttribute="trailing" constant="20" id="eSz-lz-Fdh"/>
|
||||
<constraint firstItem="qgf-Jf-cJr" firstAttribute="top" secondItem="cD5-Su-lXR" secondAttribute="bottom" constant="30" id="esg-lX-BAT"/>
|
||||
<constraint firstItem="f1p-GL-O3o" firstAttribute="leading" secondItem="4Li-ul-zIi" secondAttribute="trailing" constant="20" id="fGd-YS-phP"/>
|
||||
<constraint firstAttribute="centerX" secondItem="t8c-Fx-e5h" secondAttribute="centerX" id="h3d-Kc-q88"/>
|
||||
<constraint firstItem="BbV-3h-mmL" firstAttribute="leading" secondItem="7ua-5a-uSd" secondAttribute="trailing" constant="12" id="ioO-NJ-Jqo"/>
|
||||
<constraint firstItem="bDE-Tl-UHg" firstAttribute="leading" secondItem="4Li-ul-zIi" secondAttribute="trailing" constant="20" id="jdk-ak-soQ"/>
|
||||
<constraint firstItem="C3G-wL-u7w" firstAttribute="centerY" secondItem="lvJ-Rk-UT5" secondAttribute="centerY" id="jfs-YI-7Ae"/>
|
||||
<constraint firstItem="lvJ-Rk-UT5" firstAttribute="trailing" secondItem="KEB-eH-x2Y" secondAttribute="trailing" id="jlD-Lo-abc"/>
|
||||
<constraint firstItem="4Li-ul-zIi" firstAttribute="leading" secondItem="oFj-ol-xpL" secondAttribute="trailing" constant="20" id="kOG-Cj-hFG"/>
|
||||
<constraint firstItem="pc8-G9-4pJ" firstAttribute="top" secondItem="qgf-Jf-cJr" secondAttribute="bottom" constant="8" id="lWU-tC-vWg"/>
|
||||
<constraint firstItem="oFj-ol-xpL" firstAttribute="trailing" secondItem="lvJ-Rk-UT5" secondAttribute="trailing" id="lse-kg-lA2"/>
|
||||
<constraint firstItem="eQb-0a-76J" firstAttribute="top" secondItem="KEB-eH-x2Y" secondAttribute="bottom" constant="8" id="m2z-1O-ifB"/>
|
||||
<constraint firstItem="d9e-Wv-Y5H" firstAttribute="trailing" secondItem="KEB-eH-x2Y" secondAttribute="trailing" id="pdq-a6-Y73"/>
|
||||
<constraint firstItem="5D8-GP-a4l" firstAttribute="top" secondItem="h6f-PY-cc0" secondAttribute="bottom" constant="25" id="lYd-VZ-lBs"/>
|
||||
<constraint firstItem="7ua-5a-uSd" firstAttribute="top" secondItem="5D8-GP-a4l" secondAttribute="bottom" priority="900" constant="25" id="pCX-eX-erN"/>
|
||||
<constraint firstAttribute="centerX" secondItem="7ua-5a-uSd" secondAttribute="centerX" constant="61" id="phL-j9-rPq"/>
|
||||
<constraint firstItem="bDE-Tl-UHg" firstAttribute="top" secondItem="pc8-G9-4pJ" secondAttribute="bottom" constant="8" id="pis-of-f93"/>
|
||||
<constraint firstItem="4Li-ul-zIi" firstAttribute="leading" secondItem="lvJ-Rk-UT5" secondAttribute="trailing" constant="20" id="qKi-KT-jzJ"/>
|
||||
<constraint firstItem="C3G-wL-u7w" firstAttribute="bottom" secondItem="PXc-xv-A28" secondAttribute="top" constant="-8" id="snd-8T-LjC"/>
|
||||
<constraint firstItem="4Li-ul-zIi" firstAttribute="leading" secondItem="d9e-Wv-Y5H" secondAttribute="trailing" constant="20" id="stz-Vm-Kxo"/>
|
||||
<constraint firstItem="PXc-xv-A28" firstAttribute="leading" secondItem="4Li-ul-zIi" secondAttribute="trailing" constant="20" id="tAa-1s-xVZ"/>
|
||||
<constraint firstItem="eQb-0a-76J" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="u1y-6V-moc"/>
|
||||
<constraint firstItem="d9e-Wv-Y5H" firstAttribute="width" secondItem="eQb-0a-76J" secondAttribute="width" id="u4p-1B-x5B"/>
|
||||
<constraint firstAttribute="bottom" secondItem="BbV-3h-mmL" secondAttribute="bottom" constant="35" id="ukF-FH-DE8"/>
|
||||
<constraint firstItem="pc8-G9-4pJ" firstAttribute="bottom" secondItem="C3G-wL-u7w" secondAttribute="top" constant="-8" id="zst-nc-VqA"/>
|
||||
<constraint firstItem="4Li-ul-zIi" firstAttribute="leading" secondItem="YNz-ka-cBi" secondAttribute="trailing" constant="20" id="vfq-83-tKI"/>
|
||||
<constraint firstItem="pDa-fA-vnC" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="z6s-ga-iAk"/>
|
||||
</constraints>
|
||||
<animations/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="initialFirstResponder" destination="kiB-jZ-69S" id="I96-dS-lq5"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="112.5" y="307.5"/>
|
||||
<point key="canvasLocation" x="302.5" y="304.5"/>
|
||||
</window>
|
||||
</objects>
|
||||
<resources>
|
||||
|
||||
21
Source/SantaGUI/SNTAccessibleTextField.h
Normal file
21
Source/SantaGUI/SNTAccessibleTextField.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
/**
|
||||
An NSTextField subclass that provides an accessiblity label equal to:
|
||||
(self.toolTip + self.stringValue) where available. It also sets the
|
||||
accessibilityRoleDescription to "label".
|
||||
*/
|
||||
@interface SNTAccessibleTextField : NSTextField
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,20 +12,28 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "SantaCachedDecision.h"
|
||||
#import "SNTAccessibleTextField.h"
|
||||
|
||||
OSDefineMetaClassAndStructors(SantaCachedDecision, OSObject);
|
||||
@implementation SNTAccessibleTextField
|
||||
|
||||
uint64_t SantaCachedDecision::getMicrosecs() const {
|
||||
return microsecs_;
|
||||
- (BOOL)accessibilityIsIgnored {
|
||||
return NO;
|
||||
}
|
||||
|
||||
santa_action_t SantaCachedDecision::getAction() const {
|
||||
return action_;
|
||||
- (NSString *)accessibilityLabel {
|
||||
if (self.toolTip && self.stringValue) {
|
||||
return [NSString stringWithFormat:@"%@: %@", self.toolTip, self.stringValue];
|
||||
} else if (self.stringValue) {
|
||||
return self.stringValue;
|
||||
} else if (self.toolTip) {
|
||||
return self.toolTip;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
void SantaCachedDecision::setAction(
|
||||
const santa_action_t action, const uint64_t microsecs) {
|
||||
action_ = action;
|
||||
microsecs_ = microsecs;
|
||||
- (NSString *)accessibilityRoleDescription {
|
||||
return @"label";
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -18,6 +18,7 @@
|
||||
#import "SNTConfigurator.h"
|
||||
#import "SNTFileWatcher.h"
|
||||
#import "SNTNotificationManager.h"
|
||||
#import "SNTStrengthify.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@@ -36,8 +37,8 @@
|
||||
[self setupMenu];
|
||||
|
||||
self.configFileWatcher = [[SNTFileWatcher alloc] initWithFilePath:kDefaultConfigFilePath
|
||||
handler:^{
|
||||
[[SNTConfigurator configurator] reloadConfigData];
|
||||
handler:^(unsigned long data) {
|
||||
if (! (data & DISPATCH_VNODE_ATTRIB)) [[SNTConfigurator configurator] reloadConfigData];
|
||||
}];
|
||||
|
||||
self.notificationManager = [[SNTNotificationManager alloc] init];
|
||||
@@ -71,28 +72,37 @@
|
||||
#pragma mark Connection handling
|
||||
|
||||
- (void)createConnection {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
NSXPCListener *listener = [NSXPCListener anonymousListener];
|
||||
self.listener = [[SNTXPCConnection alloc] initServerWithListener:listener];
|
||||
self.listener.exportedInterface = [SNTXPCNotifierInterface notifierInterface];
|
||||
self.listener.exportedObject = self.notificationManager;
|
||||
self.listener.invalidationHandler = ^{
|
||||
[weakSelf attemptReconnection];
|
||||
};
|
||||
[self.listener resume];
|
||||
WEAKIFY(self);
|
||||
|
||||
SNTXPCConnection *daemonConn = [SNTXPCControlInterface configuredConnection];
|
||||
[daemonConn resume];
|
||||
[[daemonConn remoteObjectProxy] setNotificationListener:listener.endpoint];
|
||||
});
|
||||
// Create listener for return connection from daemon.
|
||||
NSXPCListener *listener = [NSXPCListener anonymousListener];
|
||||
self.listener = [[SNTXPCConnection alloc] initServerWithListener:listener];
|
||||
self.listener.exportedInterface = [SNTXPCNotifierInterface notifierInterface];
|
||||
self.listener.exportedObject = self.notificationManager;
|
||||
self.listener.acceptedHandler = ^{
|
||||
dispatch_semaphore_signal(sema);
|
||||
};
|
||||
self.listener.invalidationHandler = ^{
|
||||
STRONGIFY(self);
|
||||
[self attemptReconnection];
|
||||
};
|
||||
[self.listener resume];
|
||||
|
||||
// Tell daemon to connect back to the above listener.
|
||||
SNTXPCConnection *daemonConn = [SNTXPCControlInterface configuredConnection];
|
||||
[daemonConn resume];
|
||||
[[daemonConn remoteObjectProxy] setNotificationListener:listener.endpoint];
|
||||
|
||||
// Now wait for the connection to come in.
|
||||
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
[self attemptReconnection];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)attemptReconnection {
|
||||
// TODO(rah): Make this smarter.
|
||||
sleep(5);
|
||||
[self createConnection];
|
||||
[self performSelectorInBackground:@selector(createConnection) withObject:nil];
|
||||
}
|
||||
|
||||
#pragma mark Menu Management
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@class SNTStoredEvent;
|
||||
|
||||
@protocol SNTMessageWindowControllerDelegate
|
||||
- (void)windowDidClose;
|
||||
- (void)windowDidCloseSilenceHash:(NSString *)hash;
|
||||
@end
|
||||
|
||||
///
|
||||
@@ -29,41 +29,9 @@
|
||||
- (IBAction)closeWindow:(id)sender;
|
||||
- (IBAction)showCertInfo:(id)sender;
|
||||
|
||||
///
|
||||
/// The execution event that this window is for
|
||||
///
|
||||
@property SNTStoredEvent *event;
|
||||
|
||||
///
|
||||
/// The custom message to display for this event
|
||||
///
|
||||
@property(copy) NSString *customMessage;
|
||||
|
||||
///
|
||||
/// The delegate to inform when the notification is dismissed
|
||||
///
|
||||
@property(weak) id<SNTMessageWindowControllerDelegate> delegate;
|
||||
|
||||
///
|
||||
/// A 'friendly' string representing the certificate information
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *publisherInfo;
|
||||
|
||||
///
|
||||
/// An optional message to display with this block.
|
||||
///
|
||||
@property(readonly, nonatomic) NSAttributedString *attributedCustomMessage;
|
||||
|
||||
///
|
||||
/// Reference to the "Open Event" button in the XIB. Used to either remove the button
|
||||
/// if it isn't needed or set its title if it is.
|
||||
///
|
||||
@property IBOutlet NSButton *openEventButton;
|
||||
|
||||
///
|
||||
/// Reference to the "Application Name" label in the XIB. Used to remove if application
|
||||
/// doesn't have a CFBundleName.
|
||||
///
|
||||
@property IBOutlet NSTextField *applicationNameLabel;
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,18 +17,44 @@
|
||||
#import <SecurityInterface/SFCertificatePanel.h>
|
||||
|
||||
#import "MOLCertificate.h"
|
||||
#import "SNTBlockMessage.h"
|
||||
#import "SNTConfigurator.h"
|
||||
#import "SNTFileInfo.h"
|
||||
#import "SNTMessageWindow.h"
|
||||
#import "SNTStoredEvent.h"
|
||||
|
||||
@interface SNTMessageWindowController ()
|
||||
/// The execution event that this window is for
|
||||
@property SNTStoredEvent *event;
|
||||
|
||||
/// The custom message to display for this event
|
||||
@property(copy) NSString *customMessage;
|
||||
|
||||
/// A 'friendly' string representing the certificate information
|
||||
@property(readonly, nonatomic) NSString *publisherInfo;
|
||||
|
||||
/// An optional message to display with this block.
|
||||
@property(readonly, nonatomic) NSAttributedString *attributedCustomMessage;
|
||||
|
||||
/// Reference to the "Open Event" button in the XIB. Used to either remove the button
|
||||
/// if it isn't needed or set its title if it is.
|
||||
@property IBOutlet NSButton *openEventButton;
|
||||
|
||||
/// Reference to the "Application Name" label in the XIB. Used to remove if application
|
||||
/// doesn't have a CFBundleName.
|
||||
@property IBOutlet NSTextField *applicationNameLabel;
|
||||
|
||||
/// Linked to checkbox in UI to prevent future notifications for this binary.
|
||||
@property BOOL silenceFutureNotifications;
|
||||
@end
|
||||
|
||||
@implementation SNTMessageWindowController
|
||||
|
||||
- (instancetype)initWithEvent:(SNTStoredEvent *)event andMessage:(NSString *)message {
|
||||
self = [super initWithWindowNibName:@"MessageWindow"];
|
||||
if (self) {
|
||||
_event = event;
|
||||
_customMessage = (message != (NSString *)[NSNull null] ? message : nil);
|
||||
_customMessage = message;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -61,7 +87,13 @@
|
||||
}
|
||||
|
||||
- (void)windowWillClose:(NSNotification *)notification {
|
||||
if (self.delegate) [self.delegate windowDidClose];
|
||||
if (!self.delegate) return;
|
||||
|
||||
if (self.silenceFutureNotifications) {
|
||||
[self.delegate windowDidCloseSilenceHash:self.event.fileSHA256];
|
||||
} else {
|
||||
[self.delegate windowDidCloseSilenceHash:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)showCertInfo:(id)sender {
|
||||
@@ -80,18 +112,9 @@
|
||||
}
|
||||
|
||||
- (IBAction)openEventDetails:(id)sender {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
NSString *formatStr = config.eventDetailURL;
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%file_sha%"
|
||||
withString:self.event.fileSHA256];
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%username%"
|
||||
withString:self.event.executingUser];
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%machine_id%"
|
||||
withString:config.machineID];
|
||||
|
||||
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event];
|
||||
[self closeWindow:sender];
|
||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:formatStr]];
|
||||
[[NSWorkspace sharedWorkspace] openURL:url];
|
||||
}
|
||||
|
||||
#pragma mark Generated properties
|
||||
@@ -100,7 +123,7 @@
|
||||
if (![key isEqualToString:@"event"]) {
|
||||
return [NSSet setWithObject:@"event"];
|
||||
} else {
|
||||
return nil;
|
||||
return [NSSet set];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,39 +142,8 @@
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedCustomMessage {
|
||||
NSString *htmlHeader = @"<html><head><style>"
|
||||
@"body {"
|
||||
@" font-family: 'Lucida Grande', 'Helvetica', sans-serif;"
|
||||
@" font-size: 13px;"
|
||||
@" color: #AAA;"
|
||||
@" text-align: center;"
|
||||
@"}"
|
||||
@"</style></head><body>";
|
||||
NSString *htmlFooter = @"</body></html>";
|
||||
|
||||
NSString *message;
|
||||
if (self.customMessage.length) {
|
||||
message = self.customMessage;
|
||||
} else if (self.event.decision == EVENTSTATE_BLOCK_UNKNOWN) {
|
||||
message = [[SNTConfigurator configurator] unknownBlockMessage];
|
||||
if (!message) {
|
||||
message = @"The following application has been blocked from executing<br />"
|
||||
@"because its trustworthiness cannot be determined.";
|
||||
}
|
||||
} else {
|
||||
message = [[SNTConfigurator configurator] bannedBlockMessage];
|
||||
if (!message) {
|
||||
message = @"The following application has been blocked from executing<br />"
|
||||
@"because it has been deemed malicious.";
|
||||
}
|
||||
}
|
||||
|
||||
NSString *fullHTML = [NSString stringWithFormat:@"%@%@%@", htmlHeader, message, htmlFooter];
|
||||
|
||||
NSData *htmlData = [fullHTML dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSAttributedString *returnStr = [[NSAttributedString alloc] initWithHTML:htmlData
|
||||
documentAttributes:NULL];
|
||||
return returnStr;
|
||||
return [SNTBlockMessage attributedBlockMessageForEvent:self.event
|
||||
customMessage:self.customMessage];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,22 +14,23 @@
|
||||
|
||||
#import "SNTNotificationManager.h"
|
||||
|
||||
#import "SNTBlockMessage.h"
|
||||
#import "SNTConfigurator.h"
|
||||
#import "SNTLogging.h"
|
||||
#import "SNTStoredEvent.h"
|
||||
|
||||
@interface SNTNotificationManager ()
|
||||
///
|
||||
/// The currently displayed notification
|
||||
///
|
||||
@property SNTMessageWindowController *currentWindowController;
|
||||
|
||||
///
|
||||
/// The queue of pending notifications
|
||||
///
|
||||
@property(readonly) NSMutableArray *pendingNotifications;
|
||||
@end
|
||||
|
||||
@implementation SNTNotificationManager
|
||||
|
||||
static NSString * const silencedNotificationsKey = @"SilencedNotifications";
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
@@ -38,7 +39,9 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)windowDidClose {
|
||||
- (void)windowDidCloseSilenceHash:(NSString *)hash {
|
||||
if (hash) [self updateSilenceDate:[NSDate date] forHash:hash];
|
||||
|
||||
[self.pendingNotifications removeObject:self.currentWindowController];
|
||||
self.currentWindowController = nil;
|
||||
|
||||
@@ -50,7 +53,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark SNTNotifierXPC protocol methods
|
||||
- (void)updateSilenceDate:(NSDate *)date forHash:(NSString *)hash {
|
||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||
NSMutableDictionary *d = [[ud objectForKey:silencedNotificationsKey] mutableCopy];
|
||||
if (!d) d = [NSMutableDictionary dictionary];
|
||||
if (date) {
|
||||
d[hash] = date;
|
||||
} else {
|
||||
[d removeObjectForKey:hash];
|
||||
}
|
||||
[ud setObject:d forKey:silencedNotificationsKey];
|
||||
}
|
||||
|
||||
#pragma mark SNTNotifierXPC protocol method
|
||||
|
||||
- (void)postClientModeNotification:(SNTClientMode)clientmode {
|
||||
NSUserNotification *un = [[NSUserNotification alloc] init];
|
||||
un.title = @"Santa";
|
||||
un.hasActionButton = NO;
|
||||
NSString *customMsg;
|
||||
switch (clientmode) {
|
||||
case SNTClientModeMonitor:
|
||||
un.informativeText = @"Switching into Monitor mode";
|
||||
customMsg = [[SNTConfigurator configurator] modeNotificationMonitor];
|
||||
customMsg = [SNTBlockMessage stringFromHTML:customMsg];
|
||||
if (customMsg.length) un.informativeText = customMsg;
|
||||
break;
|
||||
case SNTClientModeLockdown:
|
||||
un.informativeText = @"Switching into Lockdown mode";
|
||||
customMsg = [[SNTConfigurator configurator] modeNotificationLockdown];
|
||||
customMsg = [SNTBlockMessage stringFromHTML:customMsg];
|
||||
if (customMsg.length) un.informativeText = customMsg;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:un];
|
||||
}
|
||||
|
||||
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message {
|
||||
// See if this binary is already in the list of pending notifications.
|
||||
@@ -58,40 +97,42 @@
|
||||
[NSPredicate predicateWithFormat:@"event.fileSHA256==%@", event.fileSHA256];
|
||||
if ([[self.pendingNotifications filteredArrayUsingPredicate:predicate] count]) return;
|
||||
|
||||
// See if this binary is silenced.
|
||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||
NSDate *silenceDate = [ud objectForKey:silencedNotificationsKey][event.fileSHA256];
|
||||
if ([silenceDate isKindOfClass:[NSDate class]]) {
|
||||
NSDate *oneDayAgo = [NSDate dateWithTimeIntervalSinceNow:-86400];
|
||||
if ([silenceDate compare:[NSDate date]] == NSOrderedDescending) {
|
||||
LOGI(@"Notification silence: date is in the future, ignoring");
|
||||
[self updateSilenceDate:nil forHash:event.fileSHA256];
|
||||
} else if ([silenceDate compare:oneDayAgo] == NSOrderedAscending) {
|
||||
LOGI(@"Notification silence: date is more than one day ago, ignoring");
|
||||
[self updateSilenceDate:nil forHash:event.fileSHA256];
|
||||
} else {
|
||||
LOGI(@"Notification silence: dropping notification for %@", event.fileSHA256);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
NSLog(@"Error: Missing event object in message received from daemon!");
|
||||
LOGI(@"Error: Missing event object in message received from daemon!");
|
||||
return;
|
||||
}
|
||||
if (!message) message = (NSString *)[NSNull null];
|
||||
|
||||
// Notifications arrive on a background thread but UI updates must happen on the main thread.
|
||||
// This includes making windows.
|
||||
[self performSelectorOnMainThread:@selector(postBlockNotificationMainThread:)
|
||||
withObject:@{ @"event" : event,
|
||||
@"custommsg" : message }
|
||||
waitUntilDone:NO];
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
SNTMessageWindowController *pendingMsg =
|
||||
[[SNTMessageWindowController alloc] initWithEvent:event andMessage:message];
|
||||
pendingMsg.delegate = self;
|
||||
[self.pendingNotifications addObject:pendingMsg];
|
||||
|
||||
- (void)postBlockNotificationMainThread:(NSDictionary *)dict {
|
||||
SNTStoredEvent *event = dict[@"event"];
|
||||
NSString *msg = dict[@"custommsg"];
|
||||
|
||||
// Create message window
|
||||
SNTMessageWindowController *pendingMsg = [[SNTMessageWindowController alloc] initWithEvent:event
|
||||
andMessage:msg];
|
||||
pendingMsg.delegate = self;
|
||||
[self.pendingNotifications addObject:pendingMsg];
|
||||
|
||||
// If a notification isn't currently being displayed, display the incoming one.
|
||||
if (!self.currentWindowController) {
|
||||
self.currentWindowController = pendingMsg;
|
||||
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
|
||||
// It's quite likely that we're currently on a background thread, and GUI code should always be
|
||||
// on main thread. Open the window on the main thread so any code it runs is also.
|
||||
[pendingMsg showWindow:nil];
|
||||
}
|
||||
// If a notification isn't currently being displayed, display the incoming one.
|
||||
if (!self.currentWindowController) {
|
||||
self.currentWindowController = pendingMsg;
|
||||
[pendingMsg showWindow:nil];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
41
Source/common/SNTBlockMessage.h
Normal file
41
Source/common/SNTBlockMessage.h
Normal file
@@ -0,0 +1,41 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
@class SNTStoredEvent;
|
||||
|
||||
@interface SNTBlockMessage : NSObject
|
||||
|
||||
///
|
||||
/// Return a message suitable for presenting to the user.
|
||||
/// Uses either the configured message depending on the event type or a custom message
|
||||
/// if the rule that blocked this file included one.
|
||||
///
|
||||
/// In SantaGUI this will return an NSAttributedString with links and formatting included
|
||||
/// while for santad all HTML will be properly stripped.
|
||||
///
|
||||
+ (NSAttributedString *)attributedBlockMessageForEvent:(SNTStoredEvent *)event
|
||||
customMessage:(NSString *)customMessage;
|
||||
|
||||
///
|
||||
/// Return a URL generated from the EventDetailURL configuration key
|
||||
/// after replacing templates in the URL with values from the event.
|
||||
///
|
||||
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event;
|
||||
|
||||
///
|
||||
/// Strip HTML from a string, replacing <br /> with newline.
|
||||
///
|
||||
+ (NSString *)stringFromHTML:(NSString *)html;
|
||||
|
||||
@end
|
||||
128
Source/common/SNTBlockMessage.m
Normal file
128
Source/common/SNTBlockMessage.m
Normal file
@@ -0,0 +1,128 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTBlockMessage.h"
|
||||
|
||||
#import "SNTConfigurator.h"
|
||||
#import "SNTLogging.h"
|
||||
#import "SNTStoredEvent.h"
|
||||
|
||||
@implementation SNTBlockMessage
|
||||
|
||||
+ (NSAttributedString *)attributedBlockMessageForEvent:(SNTStoredEvent *)event
|
||||
customMessage:(NSString *)customMessage {
|
||||
NSString *htmlHeader = @"<html><head><style>"
|
||||
@"body {"
|
||||
@" font-family: 'Lucida Grande', 'Helvetica', sans-serif;"
|
||||
@" font-size: 13px;"
|
||||
@" color: #666;"
|
||||
@" text-align: center;"
|
||||
@"}"
|
||||
@"</style></head><body>";
|
||||
NSString *htmlFooter = @"</body></html>";
|
||||
|
||||
NSString *message;
|
||||
if (customMessage.length) {
|
||||
message = customMessage;
|
||||
} else if (event.decision == SNTEventStateBlockUnknown) {
|
||||
message = [[SNTConfigurator configurator] unknownBlockMessage];
|
||||
if (!message) {
|
||||
message = @"The following application has been blocked from executing<br />"
|
||||
@"because its trustworthiness cannot be determined.";
|
||||
}
|
||||
} else {
|
||||
message = [[SNTConfigurator configurator] bannedBlockMessage];
|
||||
if (!message) {
|
||||
message = @"The following application has been blocked from executing<br />"
|
||||
@"because it has been deemed malicious.";
|
||||
}
|
||||
}
|
||||
|
||||
NSString *fullHTML = [NSString stringWithFormat:@"%@%@%@", htmlHeader, message, htmlFooter];
|
||||
|
||||
#ifdef NSAppKitVersionNumber10_0
|
||||
NSData *htmlData = [fullHTML dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return [[NSAttributedString alloc] initWithHTML:htmlData documentAttributes:NULL];
|
||||
#else
|
||||
NSString *strippedHTML = [self stringFromHTML:fullHTML];
|
||||
if (!strippedHTML) {
|
||||
return [[NSAttributedString alloc] initWithString:@"This binary has been blocked."];
|
||||
}
|
||||
return [[NSAttributedString alloc] initWithString:strippedHTML];
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (NSString *)stringFromHTML:(NSString *)html {
|
||||
NSError *error;
|
||||
NSXMLDocument *xml = [[NSXMLDocument alloc] initWithXMLString:html options:0 error:&error];
|
||||
|
||||
if (!xml && error.code == NSXMLParserEmptyDocumentError) {
|
||||
html = [NSString stringWithFormat:@"<html><body>%@</body></html>", html];
|
||||
xml = [[NSXMLDocument alloc] initWithXMLString:html options:0 error:&error];
|
||||
if (!xml) return html;
|
||||
}
|
||||
|
||||
// Strip any HTML tags out of the message. Also remove any content inside <style> tags and
|
||||
// replace <br> elements with a newline.
|
||||
NSString *stripXslt = @"<?xml version='1.0' encoding='utf-8'?>"
|
||||
@"<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'"
|
||||
@" xmlns:xhtml='http://www.w3.org/1999/xhtml'>"
|
||||
@"<xsl:output method='text'/>"
|
||||
@"<xsl:template match='br'><xsl:text>\n</xsl:text></xsl:template>"
|
||||
@"<xsl:template match='style'/>"
|
||||
@"</xsl:stylesheet>";
|
||||
NSData *data = [xml objectByApplyingXSLTString:stripXslt arguments:NULL error:&error];
|
||||
if (error || ![data isKindOfClass:[NSData class]]) {
|
||||
return html;
|
||||
}
|
||||
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
NSString *formatStr;
|
||||
if (config.eventDetailBundleURL && event.fileBundleID) {
|
||||
formatStr = config.eventDetailBundleURL;
|
||||
} else {
|
||||
formatStr = config.eventDetailURL;
|
||||
}
|
||||
|
||||
if (!formatStr.length) return nil;
|
||||
|
||||
if (event.fileSHA256) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%file_sha%"
|
||||
withString:event.fileSHA256];
|
||||
}
|
||||
if (event.executingUser) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%username%"
|
||||
withString:event.executingUser];
|
||||
}
|
||||
if (config.machineID) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%machine_id%"
|
||||
withString:config.machineID];
|
||||
}
|
||||
if (event.fileBundleID) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%bundle_id%"
|
||||
withString:event.fileBundleID];
|
||||
}
|
||||
if (event.fileBundleVersionString) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%bundle_ver%"
|
||||
withString:event.fileBundleVersionString];
|
||||
}
|
||||
|
||||
return [NSURL URLWithString:formatStr];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,63 +12,58 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON__COMMONENUMS_H
|
||||
#define SANTA__COMMON__COMMONENUMS_H
|
||||
|
||||
///
|
||||
/// These enums are used in various places throughout the Santa client code.
|
||||
/// The integer values are also stored in the database and so shouldn't be changed.
|
||||
///
|
||||
|
||||
typedef enum {
|
||||
RULETYPE_UNKNOWN,
|
||||
typedef NS_ENUM(NSInteger, SNTRuleType) {
|
||||
SNTRuleTypeUnknown,
|
||||
|
||||
RULETYPE_BINARY = 1,
|
||||
RULETYPE_CERT = 2,
|
||||
SNTRuleTypeBinary = 1,
|
||||
SNTRuleTypeCertificate = 2,
|
||||
};
|
||||
|
||||
RULETYPE_MAX
|
||||
} santa_ruletype_t;
|
||||
typedef NS_ENUM(NSInteger, SNTRuleState) {
|
||||
SNTRuleStateUnknown,
|
||||
|
||||
typedef enum {
|
||||
RULESTATE_UNKNOWN,
|
||||
SNTRuleStateWhitelist = 1,
|
||||
SNTRuleStateBlacklist = 2,
|
||||
SNTRuleStateSilentBlacklist = 3,
|
||||
SNTRuleStateRemove = 4,
|
||||
};
|
||||
|
||||
RULESTATE_WHITELIST = 1,
|
||||
RULESTATE_BLACKLIST = 2,
|
||||
RULESTATE_SILENT_BLACKLIST = 3,
|
||||
RULESTATE_REMOVE = 4,
|
||||
typedef NS_ENUM(NSInteger, SNTClientMode) {
|
||||
SNTClientModeUnknown,
|
||||
|
||||
RULESTATE_MAX
|
||||
} santa_rulestate_t;
|
||||
SNTClientModeMonitor = 1,
|
||||
SNTClientModeLockdown = 2,
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
CLIENTMODE_UNKNOWN,
|
||||
typedef NS_ENUM(NSInteger, SNTEventState) {
|
||||
SNTEventStateUnknown,
|
||||
|
||||
CLIENTMODE_MONITOR = 1,
|
||||
CLIENTMODE_LOCKDOWN = 2,
|
||||
SNTEventStateAllowUnknown = 1,
|
||||
SNTEventStateAllowBinary = 2,
|
||||
SNTEventStateAllowCertificate = 3,
|
||||
SNTEventStateAllowScope = 4,
|
||||
|
||||
CLIENTMODE_MAX
|
||||
} santa_clientmode_t;
|
||||
SNTEventStateBlockUnknown = 5,
|
||||
SNTEventStateBlockBinary = 6,
|
||||
SNTEventStateBlockCertificate = 7,
|
||||
SNTEventStateBlockScope = 8,
|
||||
|
||||
typedef enum {
|
||||
EVENTSTATE_UNKNOWN,
|
||||
SNTEventStateBundleBinary = 9,
|
||||
};
|
||||
|
||||
EVENTSTATE_ALLOW_UNKNOWN = 1,
|
||||
EVENTSTATE_ALLOW_BINARY = 2,
|
||||
EVENTSTATE_ALLOW_CERTIFICATE = 3,
|
||||
EVENTSTATE_ALLOW_SCOPE = 4,
|
||||
|
||||
EVENTSTATE_BLOCK_UNKNOWN = 5,
|
||||
EVENTSTATE_BLOCK_BINARY = 6,
|
||||
EVENTSTATE_BLOCK_CERTIFICATE = 7,
|
||||
EVENTSTATE_BLOCK_SCOPE = 8,
|
||||
|
||||
EVENTSTATE_RELATED_BINARY = 9,
|
||||
|
||||
EVENTSTATE_MAX
|
||||
} santa_eventstate_t;
|
||||
typedef NS_ENUM(NSInteger, SNTRuleTableError) {
|
||||
SNTRuleTableErrorEmptyRuleArray,
|
||||
SNTRuleTableErrorInsertOrReplaceFailed,
|
||||
SNTRuleTableErrorInvalidRule,
|
||||
SNTRuleTableErrorMissingRequiredRule,
|
||||
SNTRuleTableErrorRemoveFailed
|
||||
};
|
||||
|
||||
static const char *kKextPath = "/Library/Extensions/santa-driver.kext";
|
||||
static const char *kSantaDPath = "/Library/Extensions/santa-driver.kext/Contents/MacOS/santad";
|
||||
static const char *kSantaCtlPath = "/Library/Extensions/santa-driver.kext/Contents/MacOS/santactl";
|
||||
|
||||
#endif // SANTA__COMMON__COMMONENUMS_H
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "SNTCommonEnums.h"
|
||||
#import "SNTCommonEnums.h"
|
||||
|
||||
///
|
||||
/// Singleton that provides an interface for managing configuration values on disk
|
||||
@@ -28,7 +28,7 @@ extern NSString *const kDefaultConfigFilePath;
|
||||
///
|
||||
/// The operating mode.
|
||||
///
|
||||
@property(nonatomic) santa_clientmode_t clientMode;
|
||||
@property(nonatomic) SNTClientMode clientMode;
|
||||
|
||||
///
|
||||
/// The regex of paths to log file changes for. Regexes are specified in ICU format.
|
||||
@@ -75,18 +75,24 @@ extern NSString *const kDefaultConfigFilePath;
|
||||
///
|
||||
/// When the user gets a block notification, a button can be displayed which will
|
||||
/// take them to a web page with more information about that event.
|
||||
/// There are two properties, one for individual binaries and one for binaries that are part
|
||||
/// of a bundle. If the latter is not set the former will be used.
|
||||
///
|
||||
/// This property contains a kind of format string to be turned into the URL to send them to.
|
||||
/// The following sequences will be replaced in the final URL:
|
||||
///
|
||||
/// %file_sha% -- SHA-256 of the file that was blocked.
|
||||
/// %machine_id% -- ID of the machine.
|
||||
/// %username% -- executing user.
|
||||
/// %bundle_id% -- bundle id of the binary, if applicable.
|
||||
/// %bundle_ver% -- bundle version of the binary, if applicable.
|
||||
///
|
||||
/// @note: This is not an NSURL because the format-string parsing is done elsewhere.
|
||||
///
|
||||
/// If this item isn't set, the Open Event button will not be displayed.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *eventDetailURL;
|
||||
@property(readonly, nonatomic) NSString *eventDetailBundleURL;
|
||||
|
||||
///
|
||||
/// Related to the above property, this string represents the text to show on the button.
|
||||
@@ -106,6 +112,18 @@ extern NSString *const kDefaultConfigFilePath;
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *bannedBlockMessage;
|
||||
|
||||
///
|
||||
/// The notification text to display when the client goes into MONITOR mode.
|
||||
/// Defaults to "Switching into Monitor mode"
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *modeNotificationMonitor;
|
||||
|
||||
///
|
||||
/// The notification text to display when the client goes into LOCKDOWN mode.
|
||||
/// Defaults to "Switching into Lockdown mode"
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *modeNotificationLockdown;
|
||||
|
||||
#pragma mark - Sync Settings
|
||||
|
||||
///
|
||||
|
||||
@@ -44,9 +44,12 @@ static NSString *const kEnablePageZeroProtectionKey = @"EnablePageZeroProtection
|
||||
|
||||
static NSString *const kMoreInfoURLKey = @"MoreInfoURL";
|
||||
static NSString *const kEventDetailURLKey = @"EventDetailURL";
|
||||
static NSString *const kEventDetailBundleURLKey = @"EventDetailBundleURL";
|
||||
static NSString *const kEventDetailTextKey = @"EventDetailText";
|
||||
static NSString *const kUnknownBlockMessage = @"UnknownBlockMessage";
|
||||
static NSString *const kBannedBlockMessage = @"BannedBlockMessage";
|
||||
static NSString *const kModeNotificationMonitor = @"ModeNotificationMonitor";
|
||||
static NSString *const kModeNotificationLockdown = @"ModeNotificationLockdown";
|
||||
|
||||
static NSString *const kSyncBaseURLKey = @"SyncBaseURL";
|
||||
static NSString *const kSyncLastSuccess = @"SyncLastSuccess";
|
||||
@@ -96,20 +99,23 @@ static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
|
||||
#pragma mark Public Interface
|
||||
|
||||
- (santa_clientmode_t)clientMode {
|
||||
int cm = [self.configData[kClientModeKey] intValue];
|
||||
if (cm > CLIENTMODE_UNKNOWN && cm < CLIENTMODE_MAX) {
|
||||
return (santa_clientmode_t)cm;
|
||||
- (SNTClientMode)clientMode {
|
||||
NSInteger cm = [self.configData[kClientModeKey] longValue];
|
||||
if (cm == SNTClientModeMonitor || cm == SNTClientModeLockdown) {
|
||||
return (SNTClientMode)cm;
|
||||
} else {
|
||||
self.configData[kClientModeKey] = @(CLIENTMODE_MONITOR);
|
||||
return CLIENTMODE_MONITOR;
|
||||
LOGE(@"Client mode was set to bad value: %ld. Resetting to MONITOR.", cm);
|
||||
self.configData[kClientModeKey] = @(SNTClientModeMonitor);
|
||||
return SNTClientModeMonitor;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setClientMode:(santa_clientmode_t)newMode {
|
||||
if (newMode > CLIENTMODE_UNKNOWN && newMode < CLIENTMODE_MAX) {
|
||||
- (void)setClientMode:(SNTClientMode)newMode {
|
||||
if (newMode == SNTClientModeMonitor || newMode == SNTClientModeLockdown) {
|
||||
self.configData[kClientModeKey] = @(newMode);
|
||||
[self saveConfigToDisk];
|
||||
} else {
|
||||
LOGW(@"Ignoring request to change client mode to %ld", newMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +195,10 @@ static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
return self.configData[kEventDetailURLKey];
|
||||
}
|
||||
|
||||
- (NSString *)eventDetailBundleURL {
|
||||
return self.configData[kEventDetailBundleURLKey];
|
||||
}
|
||||
|
||||
- (NSString *)eventDetailText {
|
||||
return self.configData[kEventDetailTextKey];
|
||||
}
|
||||
@@ -201,6 +211,14 @@ static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
return self.configData[kBannedBlockMessage];
|
||||
}
|
||||
|
||||
- (NSString *)modeNotificationMonitor {
|
||||
return self.configData[kModeNotificationMonitor];
|
||||
}
|
||||
|
||||
- (NSString *)modeNotificationLockdown {
|
||||
return self.configData[kModeNotificationLockdown];
|
||||
}
|
||||
|
||||
- (NSURL *)syncBaseURL {
|
||||
NSString *urlStr = self.configData[kSyncBaseURLKey];
|
||||
if (urlStr) {
|
||||
@@ -300,7 +318,8 @@ static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
options:NSDataReadingMappedIfSafe
|
||||
error:&error];
|
||||
if (error) {
|
||||
LOGE(@"Could not read configuration file: %@", [error localizedDescription]);
|
||||
LOGE(@"Could not read configuration file: %@, replacing.", [error localizedDescription]);
|
||||
[self saveConfigToDisk];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -310,7 +329,8 @@ static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
format:NULL
|
||||
error:&error];
|
||||
if (error) {
|
||||
LOGE(@"Could not parse configuration file: %@", [error localizedDescription]);
|
||||
LOGE(@"Could not parse configuration file: %@, replacing.", [error localizedDescription]);
|
||||
[self saveConfigToDisk];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,17 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
return nil;
|
||||
}
|
||||
|
||||
_fileHandle = [NSFileHandle fileHandleForReadingAtPath:_path];
|
||||
int fd = open([_path UTF8String], O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
if (error) {
|
||||
NSString *errStr = [NSString stringWithFormat:@"Unable to open file: %s", strerror(errno)];
|
||||
*error = [NSError errorWithDomain:@"com.google.santa.fileinfo"
|
||||
code:280
|
||||
userInfo:@{NSLocalizedDescriptionKey : errStr}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
_fileHandle = [[NSFileHandle alloc] initWithFileDescriptor:fd closeOnDealloc:YES];
|
||||
|
||||
struct stat fileStat;
|
||||
fstat(_fileHandle.fileDescriptor, &fileStat);
|
||||
@@ -102,7 +112,9 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
#pragma mark Hashing
|
||||
|
||||
- (void)hashSHA1:(NSString **)sha1 SHA256:(NSString **)sha256 {
|
||||
const int chunkSize = 4096;
|
||||
const int MAX_CHUNK_SIZE = 256 * 1024; // 256 KB
|
||||
const size_t chunkSize = _fileSize > MAX_CHUNK_SIZE ? MAX_CHUNK_SIZE : _fileSize;
|
||||
char chunk[chunkSize];
|
||||
|
||||
CC_SHA1_CTX c1;
|
||||
CC_SHA256_CTX c256;
|
||||
@@ -110,46 +122,59 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
if (sha1) CC_SHA1_Init(&c1);
|
||||
if (sha256) CC_SHA256_Init(&c256);
|
||||
|
||||
for (uint64_t offset = 0; offset < self.fileSize; offset += chunkSize) {
|
||||
@autoreleasepool {
|
||||
int readSize = 0;
|
||||
if (offset + chunkSize > self.fileSize) {
|
||||
readSize = (int)(self.fileSize - offset);
|
||||
} else {
|
||||
readSize = chunkSize;
|
||||
}
|
||||
int fd = self.fileHandle.fileDescriptor;
|
||||
|
||||
NSData *chunk = [self safeSubdataWithRange:NSMakeRange(offset, readSize)];
|
||||
if (!chunk) {
|
||||
if (sha1) CC_SHA1_Final(NULL, &c1);
|
||||
if (sha256) CC_SHA256_Final(NULL, &c256);
|
||||
return;
|
||||
}
|
||||
fcntl(fd, F_RDAHEAD, 1);
|
||||
struct radvisory radv;
|
||||
radv.ra_offset = 0;
|
||||
const int MAX_ADVISORY_READ = 10 * 1024 * 1024;
|
||||
radv.ra_count = (int)_fileSize < MAX_ADVISORY_READ ? (int)_fileSize : MAX_ADVISORY_READ;
|
||||
fcntl(fd, F_RDADVISE, &radv);
|
||||
ssize_t bytesRead;
|
||||
|
||||
if (sha1) CC_SHA1_Update(&c1, chunk.bytes, readSize);
|
||||
if (sha256) CC_SHA256_Update(&c256, chunk.bytes, readSize);
|
||||
for (uint64_t offset = 0; offset < _fileSize;) {
|
||||
bytesRead = pread(fd, chunk, chunkSize, offset);
|
||||
if (bytesRead > 0) {
|
||||
if (sha1) CC_SHA1_Update(&c1, chunk, (CC_LONG)bytesRead);
|
||||
if (sha256) CC_SHA256_Update(&c256, chunk, (CC_LONG)bytesRead);
|
||||
offset += bytesRead;
|
||||
} else if (bytesRead == -1 && errno == EINTR) {
|
||||
continue;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We turn off Read Ahead that we turned on
|
||||
fcntl(fd, F_RDAHEAD, 0);
|
||||
if (sha1) {
|
||||
unsigned char dgst[CC_SHA1_DIGEST_LENGTH];
|
||||
CC_SHA1_Final(dgst, &c1);
|
||||
|
||||
NSMutableString *buf = [[NSMutableString alloc] initWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];
|
||||
for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; ++i) {
|
||||
[buf appendFormat:@"%02x", (unsigned char)dgst[i]];
|
||||
}
|
||||
*sha1 = [buf copy];
|
||||
unsigned char digest[CC_SHA1_DIGEST_LENGTH];
|
||||
CC_SHA1_Final(digest, &c1);
|
||||
NSString *const SHA1FormatString =
|
||||
@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x";
|
||||
*sha1 = [[NSString alloc]
|
||||
initWithFormat:SHA1FormatString, digest[0], digest[1], digest[2],
|
||||
digest[3], digest[4], digest[5], digest[6], digest[7],
|
||||
digest[8], digest[9], digest[10], digest[11], digest[12],
|
||||
digest[13], digest[14], digest[15], digest[16],
|
||||
digest[17], digest[18], digest[19]];
|
||||
}
|
||||
if (sha256) {
|
||||
unsigned char dgst[CC_SHA256_DIGEST_LENGTH];
|
||||
CC_SHA256_Final(dgst, &c256);
|
||||
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
|
||||
CC_SHA256_Final(digest, &c256);
|
||||
NSString *const SHA256FormatString =
|
||||
@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
|
||||
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x";
|
||||
|
||||
NSMutableString *buf = [[NSMutableString alloc] initWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
|
||||
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; ++i) {
|
||||
[buf appendFormat:@"%02x", (unsigned char)dgst[i]];
|
||||
}
|
||||
*sha256 = [buf copy];
|
||||
*sha256 = [[NSString alloc]
|
||||
initWithFormat:SHA256FormatString, digest[0], digest[1], digest[2],
|
||||
digest[3], digest[4], digest[5], digest[6], digest[7],
|
||||
digest[8], digest[9], digest[10], digest[11], digest[12],
|
||||
digest[13], digest[14], digest[15], digest[16],
|
||||
digest[17], digest[18], digest[19], digest[20],
|
||||
digest[21], digest[22], digest[23], digest[24],
|
||||
digest[25], digest[26], digest[27], digest[28],
|
||||
digest[29], digest[30], digest[31]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,18 +224,18 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
|
||||
- (BOOL)isScript {
|
||||
const char *magic = (const char *)[[self safeSubdataWithRange:NSMakeRange(0, 2)] bytes];
|
||||
return (strncmp("#!", magic, 2) == 0);
|
||||
return (magic && memcmp("#!", magic, 2) == 0);
|
||||
}
|
||||
|
||||
- (BOOL)isXARArchive {
|
||||
const char *magic = (const char *)[[self safeSubdataWithRange:NSMakeRange(0, 4)] bytes];
|
||||
return (strncmp("xar!", magic, 4) == 0);
|
||||
return (magic && memcmp("xar!", magic, 4) == 0);
|
||||
}
|
||||
|
||||
- (BOOL)isDMG {
|
||||
NSUInteger last512 = self.fileSize - 512;
|
||||
const char *magic = (const char *)[[self safeSubdataWithRange:NSMakeRange(last512, 4)] bytes];
|
||||
return (strncmp("koly", magic, 4) == 0);
|
||||
return (magic && memcmp("koly", magic, 4) == 0);
|
||||
}
|
||||
|
||||
#pragma mark Page Zero
|
||||
@@ -230,7 +255,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
if (!lcData) return NO;
|
||||
|
||||
// This code assumes the __PAGEZERO is always the first load-command in the file.
|
||||
// Given that the OS X ABI says "the static linker creates a __PAGEZERO segment
|
||||
// Given that the macOS ABI says "the static linker creates a __PAGEZERO segment
|
||||
// as the first segment of an executable file." this should be OK.
|
||||
struct load_command *lc = (struct load_command *)[lcData bytes];
|
||||
if (lc->cmd == LC_SEGMENT) {
|
||||
@@ -273,9 +298,10 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
// Check that the full path is at least 4-levels deep:
|
||||
// e.g: /Calendar.app/Contents/MacOS/Calendar
|
||||
NSArray *pathComponents = [self.path pathComponents];
|
||||
if ([pathComponents count] < 4) return nil;
|
||||
NSUInteger pathComponentsCount = pathComponents.count;
|
||||
if (pathComponentsCount < 4) return nil;
|
||||
|
||||
pathComponents = [pathComponents subarrayWithRange:NSMakeRange(0, [pathComponents count] - 3)];
|
||||
pathComponents = [pathComponents subarrayWithRange:NSMakeRange(0, pathComponentsCount - 3)];
|
||||
NSBundle *bndl = [NSBundle bundleWithPath:[NSString pathWithComponents:pathComponents]];
|
||||
if (bndl && [bndl objectForInfoDictionaryKey:@"CFBundleIdentifier"]) self.bundleRef = bndl;
|
||||
}
|
||||
@@ -310,7 +336,8 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
}
|
||||
|
||||
- (NSString *)bundleName {
|
||||
return [[self.infoPlist objectForKey:@"CFBundleName"] description];
|
||||
return [[self.infoPlist objectForKey:@"CFBundleDisplayName"] description] ?:
|
||||
[[self.infoPlist objectForKey:@"CFBundleName"] description];
|
||||
}
|
||||
|
||||
- (NSString *)bundleVersion {
|
||||
@@ -444,7 +471,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
if (!cmdData) return nil;
|
||||
struct segment_command_64 *lc = (struct segment_command_64 *)[cmdData bytes];
|
||||
if (lc->cmd == LC_SEGMENT || lc->cmd == LC_SEGMENT_64) {
|
||||
if (strncmp(lc->segname, "__TEXT", 6) == 0) {
|
||||
if (memcmp(lc->segname, "__TEXT", 6) == 0) {
|
||||
nsects = lc->nsects;
|
||||
offset += sz_segment;
|
||||
break;
|
||||
@@ -458,7 +485,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
NSData *sectData = [self safeSubdataWithRange:NSMakeRange(offset, sz_section)];
|
||||
if (!sectData) return nil;
|
||||
struct section_64 *sect = (struct section_64 *)[sectData bytes];
|
||||
if (sect && strncmp(sect->sectname, "__info_plist", 12) == 0 && sect->size < 2000000) {
|
||||
if (sect && memcmp(sect->sectname, "__info_plist", 12) == 0 && sect->size < 2000000) {
|
||||
NSData *plistData = [self safeSubdataWithRange:NSMakeRange(sect->offset, sect->size)];
|
||||
if (!plistData) return nil;
|
||||
NSDictionary *plist;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
///
|
||||
/// Simple file watching class using dispatch sources. Will automatically
|
||||
/// reload the watch if the file is deleted. Will continue watching for
|
||||
/// reload the watch if the file is deleted and continue watching for
|
||||
/// events until deallocated.
|
||||
///
|
||||
@interface SNTFileWatcher : NSObject
|
||||
@@ -24,11 +24,10 @@
|
||||
/// Initializes the watcher and begins watching for modifications.
|
||||
///
|
||||
/// @param filePath the file to watch.
|
||||
/// @param handler the handler to call when changes happen.
|
||||
/// @param handler the handler to call when changes happen. The argument to the block is the
|
||||
/// type of change that happened as a bitmask to be compared with DISPATCH_VNODE_* constants.
|
||||
///
|
||||
/// @note Shortly after the file has been opened and monitoring has begun, the provided handler
|
||||
/// will be called.
|
||||
///
|
||||
- (instancetype)initWithFilePath:(NSString *)filePath handler:(void (^)(void))handler;
|
||||
- (nonnull instancetype)initWithFilePath:(nonnull NSString *)filePath
|
||||
handler:(nonnull void (^)(unsigned long))handler;
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
|
||||
#import "SNTFileWatcher.h"
|
||||
|
||||
#import "SNTStrengthify.h"
|
||||
|
||||
@interface SNTFileWatcher ()
|
||||
@property NSString *filePath;
|
||||
@property dispatch_source_t monitoringSource;
|
||||
@property(strong) void (^handler)(unsigned long);
|
||||
|
||||
@property(strong) void (^eventHandler)(void);
|
||||
@property(strong) void (^internalEventHandler)(void);
|
||||
@property(strong) void (^internalCancelHandler)(void);
|
||||
@property dispatch_source_t source;
|
||||
@end
|
||||
|
||||
@implementation SNTFileWatcher
|
||||
@@ -30,15 +30,13 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFilePath:(NSString *)filePath handler:(void (^)(void))handler {
|
||||
- (instancetype)initWithFilePath:(nonnull NSString *)filePath
|
||||
handler:(nonnull void (^)(unsigned long))handler {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_filePath = filePath;
|
||||
_eventHandler = handler;
|
||||
|
||||
if (!_filePath || !_eventHandler) return nil;
|
||||
|
||||
[self beginWatchingFile];
|
||||
_handler = handler;
|
||||
[self startWatchingFile];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -47,57 +45,57 @@
|
||||
[self stopWatchingFile];
|
||||
}
|
||||
|
||||
- (void)beginWatchingFile {
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
int mask = (DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE |
|
||||
DISPATCH_VNODE_EXTEND | DISPATCH_VNODE_RENAME);
|
||||
- (void)startWatchingFile {
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
|
||||
int mask = (DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME |
|
||||
DISPATCH_VNODE_WRITE | DISPATCH_VNODE_EXTEND | DISPATCH_VNODE_ATTRIB);
|
||||
|
||||
self.internalEventHandler = ^{
|
||||
unsigned long l = dispatch_source_get_data(weakSelf.monitoringSource);
|
||||
if (l & DISPATCH_VNODE_DELETE || l & DISPATCH_VNODE_RENAME) {
|
||||
if (weakSelf.monitoringSource) dispatch_source_cancel(weakSelf.monitoringSource);
|
||||
} else {
|
||||
weakSelf.eventHandler();
|
||||
dispatch_async(queue, ^{
|
||||
int fd = -1;
|
||||
while ((fd = open([self.filePath fileSystemRepresentation], O_EVTONLY | O_CLOEXEC)) < 0) {
|
||||
usleep(200000); // wait 200ms
|
||||
}
|
||||
};
|
||||
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fd, mask, queue);
|
||||
|
||||
self.internalCancelHandler = ^{
|
||||
int fd;
|
||||
WEAKIFY(self);
|
||||
|
||||
if (weakSelf.monitoringSource) {
|
||||
fd = (int)dispatch_source_get_handle(weakSelf.monitoringSource);
|
||||
close(fd);
|
||||
}
|
||||
dispatch_source_set_event_handler(self.source, ^{
|
||||
STRONGIFY(self);
|
||||
unsigned long data = dispatch_source_get_data(self.source);
|
||||
self.handler(data);
|
||||
if (data & DISPATCH_VNODE_DELETE || data & DISPATCH_VNODE_RENAME) {
|
||||
[self stopWatchingFile];
|
||||
[self startWatchingFile];
|
||||
}
|
||||
sleep(2);
|
||||
});
|
||||
|
||||
const char *filePathCString = [weakSelf.filePath fileSystemRepresentation];
|
||||
while ((fd = open(filePathCString, O_EVTONLY)) < 0) {
|
||||
usleep(1000);
|
||||
}
|
||||
dispatch_source_set_registration_handler(self.source, ^{
|
||||
STRONGIFY(self);
|
||||
self.handler(0);
|
||||
});
|
||||
|
||||
weakSelf.monitoringSource =
|
||||
dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fd, mask, queue);
|
||||
dispatch_source_set_event_handler(weakSelf.monitoringSource, weakSelf.internalEventHandler);
|
||||
dispatch_source_set_cancel_handler(weakSelf.monitoringSource, weakSelf.internalCancelHandler);
|
||||
dispatch_resume(weakSelf.monitoringSource);
|
||||
dispatch_source_set_cancel_handler(self.source, ^{
|
||||
STRONGIFY(self);
|
||||
int fd = (int)dispatch_source_get_handle(self.source);
|
||||
if (fd > 0) close(fd);
|
||||
});
|
||||
|
||||
weakSelf.eventHandler();
|
||||
};
|
||||
|
||||
dispatch_async(queue, self.internalCancelHandler);
|
||||
dispatch_resume(self.source);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)stopWatchingFile {
|
||||
if (!self.monitoringSource) return;
|
||||
if (!self.source) return;
|
||||
|
||||
int fd = (int)dispatch_source_get_handle(self.monitoringSource);
|
||||
dispatch_source_set_event_handler_f(self.monitoringSource, NULL);
|
||||
dispatch_source_set_cancel_handler(self.monitoringSource, ^{
|
||||
int fd = (int)dispatch_source_get_handle(self.source);
|
||||
dispatch_source_set_event_handler_f(self.source, NULL);
|
||||
dispatch_source_set_cancel_handler(self.source, ^{
|
||||
close(fd);
|
||||
});
|
||||
|
||||
dispatch_source_cancel(self.monitoringSource);
|
||||
self.monitoringSource = nil;
|
||||
dispatch_source_cancel(self.source);
|
||||
self.source = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -35,6 +35,7 @@ enum SantaDriverMethods {
|
||||
kSantaUserClientDenyBinary,
|
||||
kSantaUserClientClearCache,
|
||||
kSantaUserClientCacheCount,
|
||||
kSantaUserClientCheckCache,
|
||||
|
||||
// Any methods supported by the driver should be added above this line to
|
||||
// ensure this remains the count of methods.
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
|
||||
#ifdef KERNEL
|
||||
|
||||
#include <IOKit/IOLib.h>
|
||||
|
||||
#ifdef DEBUG
|
||||
#define LOGD(...) IOLog("D santa-driver: " __VA_ARGS__); IOLog("\n");
|
||||
#define LOGD(...) IOLog("D santa-driver: " __VA_ARGS__); IOLog("\n")
|
||||
#else // DEBUG
|
||||
#define LOGD(...)
|
||||
#endif // DEBUG
|
||||
|
||||
@@ -42,10 +42,8 @@ void logMessage(LogLevel level, FILE *destination, NSString *format, ...) {
|
||||
}
|
||||
|
||||
// If requested, redirect output to syslog.
|
||||
if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--syslog"] ||
|
||||
strcmp(binaryName, "santad") == 0) {
|
||||
if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--syslog"]) {
|
||||
useSyslog = YES;
|
||||
|
||||
pthread_key_create(&syslogKey, syslogClientDestructor);
|
||||
}
|
||||
});
|
||||
@@ -54,7 +52,7 @@ void logMessage(LogLevel level, FILE *destination, NSString *format, ...) {
|
||||
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
NSString *s = [[NSString alloc] initWithFormat:format arguments:args];
|
||||
NSMutableString *s = [[NSMutableString alloc] initWithFormat:format arguments:args];
|
||||
va_end(args);
|
||||
|
||||
if (useSyslog) {
|
||||
@@ -88,6 +86,8 @@ void logMessage(LogLevel level, FILE *destination, NSString *format, ...) {
|
||||
|
||||
asl_log(client, NULL, syslogLevel, "%s %s: %s", levelName, binaryName, [s UTF8String]);
|
||||
} else {
|
||||
fprintf(destination, "%s\n", [s UTF8String]);
|
||||
[s appendString:@"\n"];
|
||||
size_t len = [s lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
|
||||
fwrite([s UTF8String], len, 1, destination);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "SNTCommonEnums.h"
|
||||
#import "SNTCommonEnums.h"
|
||||
|
||||
///
|
||||
/// Represents a Rule.
|
||||
@@ -27,12 +27,12 @@
|
||||
///
|
||||
/// The state of this rule
|
||||
///
|
||||
@property santa_rulestate_t state;
|
||||
@property SNTRuleState state;
|
||||
|
||||
///
|
||||
/// The type of object this rule is for (binary, certificate)
|
||||
///
|
||||
@property santa_ruletype_t type;
|
||||
@property SNTRuleType type;
|
||||
|
||||
///
|
||||
/// A custom message that will be displayed if this rule blocks a binary from executing
|
||||
@@ -43,8 +43,8 @@
|
||||
/// Designated initializer.
|
||||
///
|
||||
- (instancetype)initWithShasum:(NSString *)shasum
|
||||
state:(santa_rulestate_t)state
|
||||
type:(santa_ruletype_t)type
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg;
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
@implementation SNTRule
|
||||
|
||||
- (instancetype)initWithShasum:(NSString *)shasum
|
||||
state:(santa_rulestate_t)state
|
||||
type:(santa_ruletype_t)type
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
@@ -77,7 +77,7 @@
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"SNTRule: SHA-256: %@, State: %d, Type: %d",
|
||||
return [NSString stringWithFormat:@"SNTRule: SHA-256: %@, State: %ld, Type: %ld",
|
||||
self.shasum, self.state, self.type];
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "SNTCommonEnums.h"
|
||||
#import "SNTCommonEnums.h"
|
||||
|
||||
///
|
||||
/// Represents an event stored in the database.
|
||||
@@ -35,10 +35,16 @@
|
||||
@property NSString *filePath;
|
||||
|
||||
///
|
||||
/// If the executed file was part of the bundle, this is the CFBundleName.
|
||||
/// If the executed file was part of the bundle, this is the CFBundleDisplayName, if it exists
|
||||
/// or the CFBundleName if not.
|
||||
///
|
||||
@property NSString *fileBundleName;
|
||||
|
||||
///
|
||||
/// If the executed file was part of the bundle, this is the path to the bundle.
|
||||
///
|
||||
@property NSString *fileBundlePath;
|
||||
|
||||
///
|
||||
/// If the executed file was part of the bundle, this is the CFBundleID.
|
||||
///
|
||||
@@ -73,7 +79,7 @@
|
||||
///
|
||||
/// The decision santad returned.
|
||||
///
|
||||
@property santa_eventstate_t decision;
|
||||
@property SNTEventState decision;
|
||||
|
||||
///
|
||||
/// NSArray of logged in users when the decision was made.
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
ENCODE(self.filePath, @"filePath");
|
||||
|
||||
ENCODE(self.fileBundleName, @"fileBundleName");
|
||||
ENCODE(self.fileBundlePath, @"fileBundlePath");
|
||||
ENCODE(self.fileBundleID, @"fileBundleID");
|
||||
ENCODE(self.fileBundleVersion, @"fileBundleVersion");
|
||||
ENCODE(self.fileBundleVersionString, @"fileBundleVersionString");
|
||||
@@ -64,6 +65,7 @@
|
||||
_filePath = DECODE(NSString, @"filePath");
|
||||
|
||||
_fileBundleName = DECODE(NSString, @"fileBundleName");
|
||||
_fileBundlePath = DECODE(NSString, @"fileBundlePath");
|
||||
_fileBundleID = DECODE(NSString, @"fileBundleID");
|
||||
_fileBundleVersion = DECODE(NSString, @"fileBundleVersion");
|
||||
_fileBundleVersionString = DECODE(NSString, @"fileBundleVersionString");
|
||||
@@ -72,7 +74,7 @@
|
||||
|
||||
_executingUser = DECODE(NSString, @"executingUser");
|
||||
_occurrenceDate = DECODE(NSDate, @"occurrenceDate");
|
||||
_decision = (santa_eventstate_t)[DECODE(NSNumber, @"decision") intValue];
|
||||
_decision = (SNTEventState)[DECODE(NSNumber, @"decision") intValue];
|
||||
_pid = DECODE(NSNumber, @"pid");
|
||||
_ppid = DECODE(NSNumber, @"ppid");
|
||||
_parentName = DECODE(NSString, @"parentName");
|
||||
|
||||
22
Source/common/SNTStrengthify.h
Normal file
22
Source/common/SNTStrengthify.h
Normal file
@@ -0,0 +1,22 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#define STRONGIFY(var) \
|
||||
_Pragma("clang diagnostic push") \
|
||||
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
|
||||
__strong __typeof(var) var = (Weak_##var); \
|
||||
_Pragma("clang diagnostic pop")
|
||||
|
||||
#define WEAKIFY(var) \
|
||||
__weak __typeof(var) Weak_##var = (var);
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
/**
|
||||
A wrapper around NSXPCListener and NSXPCConnection to provide client multiplexing, signature
|
||||
validation of connecting clients and a simpler interface.
|
||||
validation of connecting clients and forced connection establishment.
|
||||
|
||||
Example server started by @c launchd where the @c launchd job has a @c MachServices key:
|
||||
|
||||
@@ -40,7 +40,12 @@
|
||||
@code
|
||||
[conn.remoteObjectProxy selectorInRemoteInterface];
|
||||
@endcode
|
||||
|
||||
|
||||
One advantage of the way that SNTXPCConnection works over using NSXPCConnection directly is that
|
||||
from the client-side once the resume method has finished, the connection is either valid or the
|
||||
invalidation handler will be called. Ordinarily, the connection doesn't actually get made until
|
||||
the first message is sent across it.
|
||||
|
||||
@note messages are always delivered on a background thread!
|
||||
*/
|
||||
@interface SNTXPCConnection : NSObject<NSXPCListenerDelegate>
|
||||
@@ -48,14 +53,14 @@
|
||||
/**
|
||||
Initialize a new server with a given listener, provided by `[NSXPCListener anonymousListener]`.
|
||||
*/
|
||||
- (instancetype)initServerWithListener:(NSXPCListener *)listener;
|
||||
- (nullable instancetype)initServerWithListener:(nonnull NSXPCListener *)listener;
|
||||
|
||||
/**
|
||||
Initializer for the 'server' side of the connection, started by launchd.
|
||||
|
||||
@param name MachService name, must match the MachServices key in the launchd.plist
|
||||
*/
|
||||
- (instancetype)initServerWithName:(NSString *)name;
|
||||
- (nullable instancetype)initServerWithName:(nonnull NSString *)name;
|
||||
|
||||
/**
|
||||
Initializer a new client to a service exported by a LaunchDaemon.
|
||||
@@ -63,17 +68,20 @@
|
||||
@param name MachService name
|
||||
@param privileged Use YES if the server is running as root.
|
||||
*/
|
||||
- (instancetype)initClientWithName:(NSString *)name privileged:(BOOL)privileged;
|
||||
- (nullable instancetype)initClientWithName:(nonnull NSString *)name privileged:(BOOL)privileged;
|
||||
|
||||
/**
|
||||
Initialize a new client with a listener endpoint sent from another process.
|
||||
|
||||
@param listener An NSXPCListenerEndpoint to connect to.
|
||||
*/
|
||||
- (instancetype)initClientWithListener:(NSXPCListenerEndpoint *)listener;
|
||||
- (nullable instancetype)initClientWithListener:(nonnull NSXPCListenerEndpoint *)listener;
|
||||
|
||||
/**
|
||||
Call when the properties of the object have been set-up and you're ready for connections.
|
||||
|
||||
For clients, this call can take up to 2s to complete for connection to finish establishing though
|
||||
in basically all cases it will actually complete in a few milliseconds.
|
||||
*/
|
||||
- (void)resume;
|
||||
|
||||
@@ -84,27 +92,35 @@
|
||||
|
||||
/**
|
||||
The interface the remote object should conform to. (client)
|
||||
*/
|
||||
@property(retain) NSXPCInterface *remoteInterface;
|
||||
*/
|
||||
@property(retain, nullable) NSXPCInterface *remoteInterface;
|
||||
|
||||
/**
|
||||
A proxy to the object at the other end of the connection. (client)
|
||||
*/
|
||||
@property(readonly, nonatomic) id remoteObjectProxy;
|
||||
|
||||
@note If the connection to the server failed, this will be nil, so you can safely send messages
|
||||
and rely on the invalidationHandler for handling the failure.
|
||||
*/
|
||||
@property(readonly, nonatomic, nullable) id remoteObjectProxy;
|
||||
|
||||
/**
|
||||
The interface this object exports. (server)
|
||||
*/
|
||||
@property(retain) NSXPCInterface *exportedInterface;
|
||||
*/
|
||||
@property(retain, nullable) NSXPCInterface *exportedInterface;
|
||||
|
||||
/**
|
||||
The object that responds to messages from the other end. (server)
|
||||
*/
|
||||
@property(retain) id exportedObject;
|
||||
*/
|
||||
@property(retain, nullable) id exportedObject;
|
||||
|
||||
/**
|
||||
A block to run when a/the connection is invalidated/interrupted.
|
||||
*/
|
||||
@property(copy) void (^invalidationHandler)(void);
|
||||
A block to run when a/the connection is accepted and fully established.
|
||||
*/
|
||||
@property(copy, nullable) void (^acceptedHandler)(void);
|
||||
|
||||
/**
|
||||
A block to run when a/the connection is invalidated/interrupted/rejected.
|
||||
*/
|
||||
@property(copy, nullable) void (^invalidationHandler)(void);
|
||||
|
||||
@end
|
||||
|
||||
@@ -16,11 +16,39 @@
|
||||
|
||||
#import "MOLCodesignChecker.h"
|
||||
|
||||
#import "SNTStrengthify.h"
|
||||
|
||||
/**
|
||||
Protocol used during connection establishment, @see SNTXPCConnectionInterface
|
||||
*/
|
||||
@protocol SNTXPCConnectionProtocol
|
||||
- (void)connectWithReply:(void (^)())reply;
|
||||
@end
|
||||
|
||||
/**
|
||||
Recipient object used during connection establishment. Each incoming connection
|
||||
has one of these objects created which accept the message in the protocol
|
||||
and call the block provided during creation before replying.
|
||||
|
||||
This allows the server to reset the connection's exported interface and
|
||||
object to the correct values after the client has sent the establishment message.
|
||||
*/
|
||||
@interface SNTXPCConnectionInterface : NSObject<SNTXPCConnectionProtocol>
|
||||
@property(strong) void (^block)(void);
|
||||
@end
|
||||
|
||||
@implementation SNTXPCConnectionInterface
|
||||
- (void)connectWithReply:(void (^)())reply {
|
||||
if (self.block) self.block();
|
||||
reply();
|
||||
}
|
||||
@end
|
||||
|
||||
@interface SNTXPCConnection ()
|
||||
@property NSXPCInterface *validationInterface;
|
||||
|
||||
/// The XPC listener (server only).
|
||||
@property NSXPCListener *listenerObject;
|
||||
/// Array of accepted connections (server only).
|
||||
@property NSMutableArray *acceptedConnections;
|
||||
|
||||
/// The current connection object (client only).
|
||||
@property NSXPCConnection *currentConnection;
|
||||
@@ -34,8 +62,8 @@
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_listenerObject = listener;
|
||||
if (!_listenerObject) return nil;
|
||||
_acceptedConnections = [NSMutableArray array];
|
||||
_validationInterface =
|
||||
[NSXPCInterface interfaceWithProtocol:@protocol(SNTXPCConnectionProtocol)];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -49,6 +77,8 @@
|
||||
if (self) {
|
||||
_currentConnection = [[NSXPCConnection alloc] initWithListenerEndpoint:listener];
|
||||
if (!_currentConnection) return nil;
|
||||
_validationInterface =
|
||||
[NSXPCInterface interfaceWithProtocol:@protocol(SNTXPCConnectionProtocol)];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -59,6 +89,8 @@
|
||||
NSXPCConnectionOptions options = (privileged ? NSXPCConnectionPrivileged : 0);
|
||||
_currentConnection = [[NSXPCConnection alloc] initWithMachServiceName:name options:options];
|
||||
if (!_currentConnection) return nil;
|
||||
_validationInterface =
|
||||
[NSXPCInterface interfaceWithProtocol:@protocol(SNTXPCConnectionProtocol)];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -75,10 +107,29 @@
|
||||
self.listenerObject.delegate = self;
|
||||
[self.listenerObject resume];
|
||||
} else {
|
||||
self.currentConnection.remoteObjectInterface = self.remoteInterface;
|
||||
WEAKIFY(self);
|
||||
|
||||
// Set-up the connection with the remote interface set to the validation interface,
|
||||
// send a message to the listener to finish establishing the connection
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
self.currentConnection.remoteObjectInterface = self.validationInterface;
|
||||
self.currentConnection.interruptionHandler = self.invalidationHandler;
|
||||
self.currentConnection.invalidationHandler = self.invalidationHandler;
|
||||
[self.currentConnection resume];
|
||||
[[self.currentConnection remoteObjectProxy] connectWithReply:^{
|
||||
STRONGIFY(self);
|
||||
// The connection is now established
|
||||
[self.currentConnection suspend];
|
||||
self.currentConnection.remoteObjectInterface = self.remoteInterface;
|
||||
[self.currentConnection resume];
|
||||
dispatch_semaphore_signal(sema);
|
||||
if (self.acceptedHandler) self.acceptedHandler();
|
||||
}];
|
||||
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC))) {
|
||||
// Connection was not established in a reasonable time, invalidate.
|
||||
self.currentConnection.remoteObjectInterface = nil; // ensure clients don't try to use it.
|
||||
[self.currentConnection invalidate];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,23 +140,36 @@
|
||||
return NO;
|
||||
}
|
||||
|
||||
[self.acceptedConnections addObject:connection];
|
||||
// The client passed the code signature check, now we need to resume the listener and
|
||||
// return YES so that the client can send the connectWithReply message. Once the client does
|
||||
// we reset the connection's exportedInterface and exportedObject.
|
||||
SNTXPCConnectionInterface *ci = [[SNTXPCConnectionInterface alloc] init];
|
||||
WEAKIFY(self);
|
||||
WEAKIFY(connection);
|
||||
ci.block = ^{
|
||||
STRONGIFY(self)
|
||||
STRONGIFY(connection);
|
||||
[connection suspend];
|
||||
connection.invalidationHandler = connection.interruptionHandler = ^{
|
||||
if (self.invalidationHandler) self.invalidationHandler();
|
||||
};
|
||||
connection.exportedInterface = self.exportedInterface;
|
||||
connection.exportedObject = self.exportedObject;
|
||||
[connection resume];
|
||||
|
||||
__weak __typeof(connection) weakConnection = connection;
|
||||
connection.interruptionHandler = connection.invalidationHandler = ^{
|
||||
[self.acceptedConnections removeObject:weakConnection];
|
||||
if (self.invalidationHandler) self.invalidationHandler();
|
||||
// The connection is now established.
|
||||
if (self.acceptedHandler) self.acceptedHandler();
|
||||
};
|
||||
|
||||
connection.exportedInterface = self.exportedInterface;
|
||||
connection.exportedObject = self.exportedObject;
|
||||
|
||||
connection.exportedInterface = self.validationInterface;
|
||||
connection.exportedObject = ci;
|
||||
[connection resume];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (id)remoteObjectProxy {
|
||||
if (self.currentConnection.remoteObjectInterface) {
|
||||
if (self.currentConnection.remoteObjectInterface &&
|
||||
self.currentConnection.remoteObjectInterface != self.validationInterface) {
|
||||
return [self.currentConnection remoteObjectProxyWithErrorHandler:^(NSError *error) {
|
||||
[self.currentConnection invalidate];
|
||||
}];
|
||||
@@ -119,11 +183,8 @@
|
||||
if (self.currentConnection) {
|
||||
[self.currentConnection invalidate];
|
||||
self.currentConnection = nil;
|
||||
} else if (self.acceptedConnections.count) {
|
||||
for (NSXPCConnection *conn in self.acceptedConnections) {
|
||||
[conn invalidate];
|
||||
}
|
||||
[self.acceptedConnections removeAllObjects];
|
||||
} else if (self.listenerObject) {
|
||||
[self.listenerObject invalidate];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "SNTCommonEnums.h"
|
||||
#import "SNTCommonEnums.h"
|
||||
#import "SNTKernelCommon.h"
|
||||
|
||||
@class SNTRule;
|
||||
@class SNTStoredEvent;
|
||||
@@ -28,29 +29,31 @@
|
||||
///
|
||||
- (void)cacheCount:(void (^)(int64_t))reply;
|
||||
- (void)flushCache:(void (^)(BOOL))reply;
|
||||
- (void)checkCacheForVnodeID:(uint64_t)vnodeID withReply:(void (^)(santa_action_t))reply;
|
||||
|
||||
///
|
||||
/// Database ops
|
||||
///
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate))reply;
|
||||
- (void)databaseRuleAddRule:(SNTRule *)rule
|
||||
cleanSlate:(BOOL)cleanSlate
|
||||
reply:(void (^)(BOOL success))reply;
|
||||
- (void)databaseRuleAddRules:(NSArray *)rules
|
||||
cleanSlate:(BOOL)cleanSlate
|
||||
reply:(void (^)(BOOL success))reply;
|
||||
reply:(void (^)(NSError *error))reply;
|
||||
|
||||
- (void)databaseEventCount:(void (^)(int64_t count))reply;
|
||||
- (void)databaseEventForSHA256:(NSString *)sha256 reply:(void (^)(SNTStoredEvent *))reply;
|
||||
- (void)databaseEventsPending:(void (^)(NSArray *events))reply;
|
||||
- (void)databaseRemoveEventsWithIDs:(NSArray *)ids;
|
||||
- (void)databaseBinaryRuleForSHA256:(NSString *)sha256 reply:(void (^)(SNTRule *))reply;
|
||||
- (void)databaseCertificateRuleForSHA256:(NSString *)sha256 reply:(void (^)(SNTRule *))reply;
|
||||
|
||||
///
|
||||
/// Config ops
|
||||
///
|
||||
- (void)watchdogInfo:(void (^)(uint64_t, uint64_t, double, double))reply;
|
||||
- (void)clientMode:(void (^)(santa_clientmode_t))reply;
|
||||
- (void)setClientMode:(santa_clientmode_t)mode reply:(void (^)())reply;
|
||||
- (void)clientMode:(void (^)(SNTClientMode))reply;
|
||||
- (void)setClientMode:(SNTClientMode)mode reply:(void (^)())reply;
|
||||
- (void)xsrfToken:(void (^)(NSString *))reply;
|
||||
- (void)setXsrfToken:(NSString *)token reply:(void (^)())reply;
|
||||
- (void)setNextSyncInterval:(uint64_t)seconds reply:(void (^)())reply;
|
||||
- (void)setSyncLastSuccess:(NSDate *)date reply:(void (^)())reply;
|
||||
- (void)setSyncCleanRequired:(BOOL)cleanReqd reply:(void (^)())reply;
|
||||
|
||||
@@ -12,11 +12,14 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommonEnums.h"
|
||||
|
||||
@class SNTStoredEvent;
|
||||
|
||||
/// Protocol implemented by SantaGUI and utilized by santad
|
||||
@protocol SNTNotifierXPC
|
||||
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message;
|
||||
- (void)postClientModeNotification:(SNTClientMode)clientmode;
|
||||
@end
|
||||
|
||||
@interface SNTXPCNotifierInterface : NSObject
|
||||
|
||||
273
Source/santa-driver/SantaCache.h
Normal file
273
Source/santa-driver/SantaCache.h
Normal file
@@ -0,0 +1,273 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTA_DRIVER__SANTACACHE_H
|
||||
#define SANTA__SANTA_DRIVER__SANTACACHE_H
|
||||
|
||||
#include <libkern/OSAtomic.h>
|
||||
#include <libkern/OSTypes.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/cdefs.h>
|
||||
|
||||
#include "SNTKernelCommon.h"
|
||||
|
||||
#define likely(x) __builtin_expect((x), 1)
|
||||
#define unlikely(x) __builtin_expect((x), 0)
|
||||
|
||||
#ifdef KERNEL
|
||||
#include <IOKit/IOLib.h>
|
||||
#else // KERNEL
|
||||
// Support for unit testing.
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#define panic(args...) printf(args); printf("\n"); abort()
|
||||
#define IOMalloc malloc
|
||||
#define IOMallocAligned(sz, alignment) malloc(sz);
|
||||
#define IOFree(addr, sz) free(addr)
|
||||
#define IOFreeAligned(addr, sz) free(addr)
|
||||
#define OSTestAndSet OSAtomicTestAndSet
|
||||
#define OSTestAndClear(bit, addr) OSAtomicTestAndClear(bit, addr) == 0
|
||||
#define OSIncrementAtomic(addr) OSAtomicIncrement64((volatile int64_t *)addr)
|
||||
#define OSDecrementAtomic(addr) OSAtomicDecrement64((volatile int64_t *)addr)
|
||||
#endif // KERNEL
|
||||
|
||||
/**
|
||||
A somewhat simple, concurrent linked-list hash table intended for use in IOKit kernel extensions.
|
||||
Maps 64-bit unsigned integer keys to values.
|
||||
|
||||
Enforces a maximum size by clearing all entries if a new value
|
||||
is added that would go over the maximum size declared at creation.
|
||||
|
||||
The number of buckets is calculated as `maximum_size` / `per_bucket`
|
||||
rounded up to the next power of 2. Locking is done per-bucket.
|
||||
*/
|
||||
template<class T> class SantaCache {
|
||||
public:
|
||||
/**
|
||||
Initialize a newly created cache.
|
||||
|
||||
@param maximum_size The maximum number of entries in this cache. Once this
|
||||
number is reached all the entries will be purged.
|
||||
@param per_bucket The target number of entries in each bucket when cache is full.
|
||||
A higher number will result in better performance but higher memory usage.
|
||||
Cannot be higher than 64 to try and ensure buckets don't overflow.
|
||||
*/
|
||||
SantaCache(uint64_t maximum_size = 10000, uint8_t per_bucket = 5) {
|
||||
if (unlikely(per_bucket < 1)) per_bucket = 1;
|
||||
if (unlikely(per_bucket > 64)) per_bucket = 64;
|
||||
max_size_ = maximum_size;
|
||||
bucket_count_ = 1 << (32 - __builtin_clz(
|
||||
((uint32_t)max_size_ / per_bucket) - 1));
|
||||
buckets_ = (struct bucket *)IOMalloc(bucket_count_ * sizeof(struct bucket));
|
||||
bzero(buckets_, bucket_count_ * sizeof(struct bucket));
|
||||
}
|
||||
|
||||
/**
|
||||
Clear and free memory
|
||||
*/
|
||||
~SantaCache() {
|
||||
clear();
|
||||
IOFree(buckets_, bucket_count_ * sizeof(struct bucket));
|
||||
}
|
||||
|
||||
/**
|
||||
Get an element from the cache. Returns zero_ if item doesn't exist.
|
||||
*/
|
||||
T get(uint64_t key) {
|
||||
struct bucket *bucket = &buckets_[hash(key)];
|
||||
lock(bucket);
|
||||
struct entry *entry = (struct entry *)((uintptr_t)bucket->head - 1);
|
||||
while (entry != nullptr) {
|
||||
if (entry->key == key) {
|
||||
unlock(bucket);
|
||||
return entry->value;
|
||||
}
|
||||
entry = entry->next;
|
||||
}
|
||||
unlock(bucket);
|
||||
return zero_;
|
||||
}
|
||||
|
||||
/**
|
||||
Set an element in the cache.
|
||||
|
||||
@note If the cache is full when this is called, this will empty the cache before
|
||||
inserting the new value.
|
||||
|
||||
@return if an existing value was replaced, the previous value, otherwise zero_
|
||||
*/
|
||||
T set(uint64_t key, T value) {
|
||||
struct bucket *bucket = &buckets_[hash(key)];
|
||||
lock(bucket);
|
||||
struct entry *entry = (struct entry *)((uintptr_t)bucket->head - 1);
|
||||
struct entry *previous_entry = nullptr;
|
||||
while (entry != nullptr) {
|
||||
if (entry->key == key) {
|
||||
T existing_value = entry->value;
|
||||
entry->value = value;
|
||||
|
||||
if (value == zero_) {
|
||||
if (previous_entry != nullptr) {
|
||||
previous_entry->next = entry->next;
|
||||
} else {
|
||||
bucket->head = (struct entry *)((uintptr_t)entry->next + 1);
|
||||
}
|
||||
IOFreeAligned(entry, sizeof(struct entry));
|
||||
OSDecrementAtomic(&count_);
|
||||
}
|
||||
|
||||
unlock(bucket);
|
||||
return existing_value;
|
||||
}
|
||||
previous_entry = entry;
|
||||
entry = entry->next;
|
||||
}
|
||||
|
||||
// If value is zero_, we're clearing but there's nothing to clear
|
||||
// so we don't need to do anything else.
|
||||
if (value == zero_) {
|
||||
unlock(bucket);
|
||||
return zero_;
|
||||
}
|
||||
|
||||
// Check that adding this new item won't take the cache over its maximum size.
|
||||
if (count_ + 1 > max_size_) {
|
||||
unlock(bucket);
|
||||
lock(&clear_bucket_);
|
||||
// Check again in case clear has already run while waiting for lock
|
||||
if (count_ + 1 > max_size_) {
|
||||
clear();
|
||||
}
|
||||
lock(bucket);
|
||||
unlock(&clear_bucket_);
|
||||
}
|
||||
|
||||
// Allocate a new entry, set the key and value, then set the next pointer as the current
|
||||
// first entry in the bucket then make this new entry the first in the bucket.
|
||||
struct entry *new_entry = (struct entry *)IOMallocAligned(sizeof(struct entry), 2);
|
||||
new_entry->key = key;
|
||||
new_entry->value = value;
|
||||
new_entry->next = (struct entry *)((uintptr_t)bucket->head - 1);
|
||||
bucket->head = (struct entry *)((uintptr_t)new_entry + 1);
|
||||
OSIncrementAtomic(&count_);
|
||||
|
||||
unlock(bucket);
|
||||
return zero_;
|
||||
}
|
||||
|
||||
/**
|
||||
An alias for `set(key, zero_)`
|
||||
*/
|
||||
inline void remove(uint64_t key) {
|
||||
set(key, zero_);
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all entries and free bucket memory.
|
||||
*/
|
||||
void clear() {
|
||||
for (uint32_t i = 0; i < bucket_count_; ++i) {
|
||||
struct bucket *bucket = &buckets_[i];
|
||||
// We grab the lock so nothing can use this bucket while we're erasing it
|
||||
// and never release it. It'll be 'released' when the bzero call happens
|
||||
// at the end of this function.
|
||||
lock(bucket);
|
||||
|
||||
// Free the bucket's entries, if there are any.
|
||||
struct entry *entry = (struct entry *)((uintptr_t)bucket->head - 1);
|
||||
while (entry != nullptr) {
|
||||
struct entry *next_entry = entry->next;
|
||||
IOFreeAligned(entry, sizeof(struct entry));
|
||||
entry = next_entry;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset cache count, no atomicity needed as we hold all the bucket locks.
|
||||
count_ = 0;
|
||||
|
||||
// This resets all of the bucket counts and locks. Releasing the locks for
|
||||
// each bucket isn't really atomic here but each bucket will be zero'd
|
||||
// before the lock is released as the lock is the last thing in a bucket.
|
||||
bzero(buckets_, bucket_count_ * sizeof(struct bucket));
|
||||
}
|
||||
|
||||
/**
|
||||
Return number of entries currently in cache.
|
||||
*/
|
||||
inline uint64_t count() const {
|
||||
return count_;
|
||||
}
|
||||
|
||||
private:
|
||||
struct entry {
|
||||
uint64_t key;
|
||||
T value;
|
||||
struct entry *next;
|
||||
};
|
||||
|
||||
struct bucket {
|
||||
// The least significant bit of this pointer is always 0 (due to alignment),
|
||||
// so we utilize that bit as the lock for the bucket.
|
||||
struct entry *head;
|
||||
};
|
||||
|
||||
/**
|
||||
Lock a bucket. Spins until the lock is acquired.
|
||||
*/
|
||||
inline void lock(struct bucket *bucket) const {
|
||||
while (OSTestAndSet(7, (volatile uint8_t *)&bucket->head));
|
||||
}
|
||||
|
||||
/**
|
||||
Unlock a bucket. Panics if the lock wasn't locked.
|
||||
*/
|
||||
inline void unlock(struct bucket *bucket) const {
|
||||
if (unlikely(OSTestAndClear(7, (volatile uint8_t *)&bucket->head))) {
|
||||
panic("SantaCache::unlock(): Tried to unlock an unlocked lock");
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t count_ = 0;
|
||||
|
||||
uint64_t max_size_;
|
||||
uint32_t bucket_count_;
|
||||
|
||||
struct bucket *buckets_;
|
||||
|
||||
/**
|
||||
Holder for a 'zero' entry for the current type
|
||||
*/
|
||||
T zero_ = {};
|
||||
|
||||
/**
|
||||
Special bucket used when automatically clearing due to size
|
||||
to prevent two threads trying to clear at the same time and
|
||||
getting stuck.
|
||||
*/
|
||||
struct bucket clear_bucket_ = {};
|
||||
|
||||
/**
|
||||
Hash a key to determine which bucket it belongs in.
|
||||
|
||||
Multiplicative hash using a prime near to the golden ratio, per Knuth.
|
||||
This seems to have good bucket distribution generally and for the range of
|
||||
values we expect to see.
|
||||
*/
|
||||
inline uint64_t hash(uint64_t input) const {
|
||||
return (input * 11400714819323198549ul) % bucket_count_;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // SANTA__SANTA_DRIVER__SANTACACHE_H
|
||||
@@ -1,44 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTA_DRIVER__SANTACACHEDDECISION_H
|
||||
#define SANTA__SANTA_DRIVER__SANTACACHEDDECISION_H
|
||||
|
||||
#include <libkern/c++/OSObject.h>
|
||||
|
||||
#include "SNTKernelCommon.h"
|
||||
|
||||
///
|
||||
/// An OSObject subclass to store a @c santa_action_t and a timestamp.
|
||||
/// Only OSObject subclasses can be inserted into an OSDictionary.
|
||||
///
|
||||
class SantaCachedDecision : public OSObject {
|
||||
OSDeclareDefaultStructors(SantaCachedDecision)
|
||||
|
||||
public:
|
||||
// Returns the time the action was last set.
|
||||
uint64_t getMicrosecs() const;
|
||||
|
||||
// Returns the set action.
|
||||
santa_action_t getAction() const;
|
||||
|
||||
// Sets the acion and receive time.
|
||||
void setAction(const santa_action_t action, const uint64_t microsecs);
|
||||
|
||||
private:
|
||||
santa_action_t action_;
|
||||
uint64_t microsecs_;
|
||||
};
|
||||
|
||||
#endif // SANTA__SANTA_DRIVER__SANTACACHEDDECISIONWRAPPER_H
|
||||
@@ -24,23 +24,20 @@ bool SantaDecisionManager::init() {
|
||||
|
||||
sdm_lock_grp_attr_ = lck_grp_attr_alloc_init();
|
||||
sdm_lock_grp_ = lck_grp_alloc_init("santa-locks", sdm_lock_grp_attr_);
|
||||
|
||||
sdm_lock_attr_ = lck_attr_alloc_init();
|
||||
|
||||
decision_dataqueue_lock_ = lck_mtx_alloc_init(sdm_lock_grp_, sdm_lock_attr_);
|
||||
log_dataqueue_lock_ = lck_mtx_alloc_init(sdm_lock_grp_, sdm_lock_attr_);
|
||||
cached_decisions_lock_ = lck_rw_alloc_init(sdm_lock_grp_, sdm_lock_attr_);
|
||||
vnode_pid_map_lock_ = lck_rw_alloc_init(sdm_lock_grp_, sdm_lock_attr_);
|
||||
|
||||
cached_decisions_ = OSDictionary::withCapacity(1000);
|
||||
vnode_pid_map_ = OSDictionary::withCapacity(1000);
|
||||
decision_cache_ = new SantaCache<uint64_t>(10000, 2);
|
||||
vnode_pid_map_ = new SantaCache<uint64_t>(2000, 5);
|
||||
|
||||
decision_dataqueue_ = IOSharedDataQueue::withEntries(kMaxDecisionQueueEvents,
|
||||
sizeof(santa_message_t));
|
||||
decision_dataqueue_ = IOSharedDataQueue::withEntries(
|
||||
kMaxDecisionQueueEvents, sizeof(santa_message_t));
|
||||
if (!decision_dataqueue_) return kIOReturnNoMemory;
|
||||
|
||||
log_dataqueue_ = IOSharedDataQueue::withEntries(kMaxLogQueueEvents,
|
||||
sizeof(santa_message_t));
|
||||
log_dataqueue_ = IOSharedDataQueue::withEntries(
|
||||
kMaxLogQueueEvents, sizeof(santa_message_t));
|
||||
if (!log_dataqueue_) return kIOReturnNoMemory;
|
||||
|
||||
client_pid_ = 0;
|
||||
@@ -49,15 +46,8 @@ bool SantaDecisionManager::init() {
|
||||
}
|
||||
|
||||
void SantaDecisionManager::free() {
|
||||
if (cached_decisions_lock_) {
|
||||
lck_rw_free(cached_decisions_lock_, sdm_lock_grp_);
|
||||
cached_decisions_lock_ = nullptr;
|
||||
}
|
||||
|
||||
if (vnode_pid_map_lock_) {
|
||||
lck_rw_free(vnode_pid_map_lock_, sdm_lock_grp_);
|
||||
vnode_pid_map_lock_ = nullptr;
|
||||
}
|
||||
delete decision_cache_;
|
||||
delete vnode_pid_map_;
|
||||
|
||||
if (decision_dataqueue_lock_) {
|
||||
lck_mtx_free(decision_dataqueue_lock_, sdm_lock_grp_);
|
||||
@@ -86,8 +76,6 @@ void SantaDecisionManager::free() {
|
||||
|
||||
OSSafeReleaseNULL(decision_dataqueue_);
|
||||
OSSafeReleaseNULL(log_dataqueue_);
|
||||
OSSafeReleaseNULL(cached_decisions_);
|
||||
OSSafeReleaseNULL(vnode_pid_map_);
|
||||
|
||||
super::free();
|
||||
}
|
||||
@@ -97,12 +85,12 @@ void SantaDecisionManager::free() {
|
||||
void SantaDecisionManager::ConnectClient(pid_t pid) {
|
||||
if (!pid) return;
|
||||
|
||||
client_pid_ = pid;
|
||||
|
||||
// Any decisions made while the daemon wasn't
|
||||
// connected should be cleared
|
||||
ClearCache();
|
||||
|
||||
client_pid_ = pid;
|
||||
|
||||
failed_decision_queue_requests_ = 0;
|
||||
failed_log_queue_requests_ = 0;
|
||||
}
|
||||
@@ -113,7 +101,7 @@ void SantaDecisionManager::DisconnectClient(bool itDied) {
|
||||
|
||||
// Ask santad to shutdown, in case it's running.
|
||||
if (!itDied) {
|
||||
santa_message_t *message = new santa_message_t;
|
||||
auto message = new santa_message_t;
|
||||
message->action = ACTION_REQUEST_SHUTDOWN;
|
||||
PostToDecisionQueue(message);
|
||||
delete message;
|
||||
@@ -136,8 +124,9 @@ void SantaDecisionManager::DisconnectClient(bool itDied) {
|
||||
}
|
||||
|
||||
bool SantaDecisionManager::ClientConnected() const {
|
||||
proc_t p = proc_find(client_pid_);
|
||||
bool is_exiting = false;
|
||||
if (client_pid_ <= 0) return false;
|
||||
auto p = proc_find(client_pid_);
|
||||
auto is_exiting = false;
|
||||
if (p) {
|
||||
is_exiting = proc_exiting(p);
|
||||
proc_rele(p);
|
||||
@@ -168,14 +157,12 @@ IOMemoryDescriptor *SantaDecisionManager::GetLogMemoryDescriptor() const {
|
||||
#pragma mark Listener Control
|
||||
|
||||
kern_return_t SantaDecisionManager::StartListener() {
|
||||
vnode_listener_ = kauth_listen_scope(KAUTH_SCOPE_VNODE,
|
||||
vnode_scope_callback,
|
||||
reinterpret_cast<void *>(this));
|
||||
vnode_listener_ = kauth_listen_scope(
|
||||
KAUTH_SCOPE_VNODE, vnode_scope_callback, reinterpret_cast<void *>(this));
|
||||
if (!vnode_listener_) return kIOReturnInternalError;
|
||||
|
||||
fileop_listener_ = kauth_listen_scope(KAUTH_SCOPE_FILEOP,
|
||||
fileop_scope_callback,
|
||||
reinterpret_cast<void *>(this));
|
||||
fileop_listener_ = kauth_listen_scope(
|
||||
KAUTH_SCOPE_FILEOP, fileop_scope_callback, reinterpret_cast<void *>(this));
|
||||
if (!fileop_listener_) return kIOReturnInternalError;
|
||||
|
||||
LOGD("Listeners started.");
|
||||
@@ -206,113 +193,67 @@ kern_return_t SantaDecisionManager::StopListener() {
|
||||
#pragma mark Cache Management
|
||||
|
||||
void SantaDecisionManager::AddToCache(
|
||||
const char *identifier, santa_action_t decision, uint64_t microsecs) {
|
||||
if (cached_decisions_->getCount() > kMaxCacheSize) {
|
||||
// This could be made a _lot_ smarter, say only removing entries older
|
||||
// than a certain time period. However, with a kMaxCacheSize set
|
||||
// sufficiently large and a kMaxAllowCacheTimeMilliseconds set
|
||||
// sufficiently low, this should only ever occur if someone is purposefully
|
||||
// trying to make the cache grow.
|
||||
LOGI("Cache too large, flushing.");
|
||||
ClearCache();
|
||||
}
|
||||
uint64_t identifier, santa_action_t decision, uint64_t microsecs) {
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
uint64_t val = ((uint64_t)decision << 56) | (microsecs & 0xFFFFFFFFFFFFFF);
|
||||
|
||||
if (decision == ACTION_REQUEST_BINARY) {
|
||||
SantaCachedDecision *pending = new SantaCachedDecision();
|
||||
pending->setAction(ACTION_REQUEST_BINARY, 0);
|
||||
lck_rw_lock_exclusive(cached_decisions_lock_);
|
||||
cached_decisions_->setObject(identifier, pending);
|
||||
lck_rw_unlock_exclusive(cached_decisions_lock_);
|
||||
pending->release(); // it was retained when added to the dictionary
|
||||
} else {
|
||||
lck_rw_lock_exclusive(cached_decisions_lock_);
|
||||
SantaCachedDecision *pending = OSDynamicCast(
|
||||
SantaCachedDecision, cached_decisions_->getObject(identifier));
|
||||
if (pending) {
|
||||
pending->setAction(decision, microsecs);
|
||||
}
|
||||
lck_rw_unlock_exclusive(cached_decisions_lock_);
|
||||
// If a previous entry was not found and the new entry is not `REQUEST_BINARY`, remove the
|
||||
// existing entry. This is to prevent adding an ALLOW to the cache after a write has occurred.
|
||||
if (decision_cache_->set(identifier, val) == 0 && decision != ACTION_REQUEST_BINARY) {
|
||||
decision_cache_->remove(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
void SantaDecisionManager::CacheCheck(const char *identifier) {
|
||||
lck_rw_lock_shared(cached_decisions_lock_);
|
||||
bool shouldInvalidate = (cached_decisions_->getObject(identifier) != nullptr);
|
||||
if (shouldInvalidate) {
|
||||
if (!lck_rw_lock_shared_to_exclusive(cached_decisions_lock_)) {
|
||||
// shared_to_exclusive will return false if a previous reader upgraded
|
||||
// and if that happens the lock will have been unlocked. If that happens,
|
||||
// which is rare, relock exclusively.
|
||||
lck_rw_lock_exclusive(cached_decisions_lock_);
|
||||
}
|
||||
cached_decisions_->removeObject(identifier);
|
||||
lck_rw_unlock_exclusive(cached_decisions_lock_);
|
||||
} else {
|
||||
lck_rw_unlock_shared(cached_decisions_lock_);
|
||||
}
|
||||
void SantaDecisionManager::RemoveFromCache(uint64_t identifier) {
|
||||
decision_cache_->remove(identifier);
|
||||
}
|
||||
|
||||
uint64_t SantaDecisionManager::CacheCount() const {
|
||||
return cached_decisions_->getCount();
|
||||
return decision_cache_->count();
|
||||
}
|
||||
|
||||
void SantaDecisionManager::ClearCache() {
|
||||
lck_rw_lock_exclusive(cached_decisions_lock_);
|
||||
cached_decisions_->flushCollection();
|
||||
lck_rw_unlock_exclusive(cached_decisions_lock_);
|
||||
decision_cache_->clear();
|
||||
}
|
||||
|
||||
#pragma mark Decision Fetching
|
||||
|
||||
santa_action_t SantaDecisionManager::GetFromCache(const char *identifier) {
|
||||
santa_action_t result = ACTION_UNSET;
|
||||
santa_action_t SantaDecisionManager::GetFromCache(uint64_t identifier) {
|
||||
auto result = ACTION_UNSET;
|
||||
uint64_t decision_time = 0;
|
||||
|
||||
lck_rw_lock_shared(cached_decisions_lock_);
|
||||
SantaCachedDecision *cached_decision = OSDynamicCast(
|
||||
SantaCachedDecision, cached_decisions_->getObject(identifier));
|
||||
if (cached_decision) {
|
||||
result = cached_decision->getAction();
|
||||
decision_time = cached_decision->getMicrosecs();
|
||||
}
|
||||
lck_rw_unlock_shared(cached_decisions_lock_);
|
||||
uint64_t cache_val = decision_cache_->get(identifier);
|
||||
if (cache_val == 0) return result;
|
||||
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
result = (santa_action_t)(cache_val >> 56);
|
||||
decision_time = (cache_val & ~(0xFF00000000000000));
|
||||
|
||||
if (RESPONSE_VALID(result)) {
|
||||
uint64_t diff_time = GetCurrentUptime();
|
||||
|
||||
if (result == ACTION_RESPOND_ALLOW) {
|
||||
if ((kMaxAllowCacheTimeMilliseconds * 1000) > diff_time) {
|
||||
diff_time = 0;
|
||||
} else {
|
||||
diff_time -= (kMaxAllowCacheTimeMilliseconds * 1000);
|
||||
}
|
||||
} else if (result == ACTION_RESPOND_DENY) {
|
||||
if (result == ACTION_RESPOND_DENY) {
|
||||
auto diff_time = GetCurrentUptime();
|
||||
if ((kMaxDenyCacheTimeMilliseconds * 1000) > diff_time) {
|
||||
diff_time = 0;
|
||||
} else {
|
||||
diff_time -= (kMaxDenyCacheTimeMilliseconds * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (decision_time < diff_time) {
|
||||
lck_rw_lock_exclusive(cached_decisions_lock_);
|
||||
cached_decisions_->removeObject(identifier);
|
||||
lck_rw_unlock_exclusive(cached_decisions_lock_);
|
||||
return ACTION_UNSET;
|
||||
if (decision_time < diff_time) {
|
||||
decision_cache_->remove(identifier);
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
santa_action_t SantaDecisionManager::GetFromDaemon(
|
||||
santa_message_t *message, const char *vnode_id_str) {
|
||||
santa_action_t return_action = ACTION_UNSET;
|
||||
santa_action_t SantaDecisionManager::GetFromDaemon(santa_message_t *message, uint64_t identifier) {
|
||||
auto return_action = ACTION_UNSET;
|
||||
|
||||
// Wait for the daemon to respond or die.
|
||||
do {
|
||||
// Add pending request to cache.
|
||||
AddToCache(vnode_id_str, ACTION_REQUEST_BINARY, 0);
|
||||
// Add pending request to cache, to be replaced by daemon with actual response
|
||||
AddToCache(identifier, ACTION_REQUEST_BINARY, 0);
|
||||
|
||||
// Send request to daemon...
|
||||
if (!PostToDecisionQueue(message)) {
|
||||
@@ -324,13 +265,13 @@ santa_action_t SantaDecisionManager::GetFromDaemon(
|
||||
client_pid_ = 0;
|
||||
}
|
||||
LOGE("Failed to queue request for %s.", message->path);
|
||||
CacheCheck(vnode_id_str);
|
||||
RemoveFromCache(identifier);
|
||||
return ACTION_ERROR;
|
||||
}
|
||||
|
||||
do {
|
||||
IOSleep(kRequestLoopSleepMilliseconds);
|
||||
return_action = GetFromCache(vnode_id_str);
|
||||
return_action = GetFromCache(identifier);
|
||||
} while (return_action == ACTION_REQUEST_BINARY && ClientConnected());
|
||||
} while (!RESPONSE_VALID(return_action) && ClientConnected());
|
||||
|
||||
@@ -338,7 +279,7 @@ santa_action_t SantaDecisionManager::GetFromDaemon(
|
||||
if (!RESPONSE_VALID(return_action)) {
|
||||
LOGE("Daemon process did not respond correctly. Allowing executions "
|
||||
"until it comes back. Executable path: %s", message->path);
|
||||
CacheCheck(vnode_id_str);
|
||||
RemoveFromCache(identifier);
|
||||
return ACTION_ERROR;
|
||||
}
|
||||
|
||||
@@ -350,11 +291,10 @@ santa_action_t SantaDecisionManager::FetchDecision(
|
||||
const vnode_t vp,
|
||||
const uint64_t vnode_id,
|
||||
const char *vnode_id_str) {
|
||||
santa_action_t return_action = ACTION_UNSET;
|
||||
if (!ClientConnected()) return ACTION_RESPOND_ALLOW;
|
||||
|
||||
// Check to see if item is in cache
|
||||
return_action = GetFromCache(vnode_id_str);
|
||||
auto return_action = GetFromCache(vnode_id);
|
||||
|
||||
// If item was in cache return it.
|
||||
if (RESPONSE_VALID(return_action)) return return_action;
|
||||
@@ -366,45 +306,34 @@ santa_action_t SantaDecisionManager::FetchDecision(
|
||||
path[0] = '\0';
|
||||
}
|
||||
|
||||
santa_message_t *message = NewMessage();
|
||||
auto message = NewMessage(cred);
|
||||
strlcpy(message->path, path, sizeof(message->path));
|
||||
message->action = ACTION_REQUEST_BINARY;
|
||||
message->vnode_id = vnode_id;
|
||||
proc_name(message->ppid, message->pname, sizeof(message->pname));
|
||||
santa_action_t ret = GetFromDaemon(message, vnode_id_str);
|
||||
return_action = GetFromDaemon(message, vnode_id);
|
||||
delete message;
|
||||
return ret;
|
||||
return return_action;
|
||||
}
|
||||
|
||||
#pragma mark Misc
|
||||
|
||||
santa_message_t *SantaDecisionManager::NewMessage() const {
|
||||
santa_message_t *message = new santa_message_t;
|
||||
message->uid = kauth_getuid();
|
||||
message->gid = kauth_getgid();
|
||||
message->pid = proc_selfpid();
|
||||
message->ppid = proc_selfppid();
|
||||
return message;
|
||||
}
|
||||
|
||||
bool SantaDecisionManager::PostToDecisionQueue(santa_message_t *message) {
|
||||
bool kr = false;
|
||||
lck_mtx_lock(decision_dataqueue_lock_);
|
||||
kr = decision_dataqueue_->enqueue(message, sizeof(santa_message_t));
|
||||
auto kr = decision_dataqueue_->enqueue(message, sizeof(santa_message_t));
|
||||
lck_mtx_unlock(decision_dataqueue_lock_);
|
||||
return kr;
|
||||
}
|
||||
|
||||
bool SantaDecisionManager::PostToLogQueue(santa_message_t *message) {
|
||||
bool kr = false;
|
||||
lck_mtx_lock(log_dataqueue_lock_);
|
||||
kr = log_dataqueue_->enqueue(message, sizeof(santa_message_t));
|
||||
auto kr = log_dataqueue_->enqueue(message, sizeof(santa_message_t));
|
||||
if (!kr) {
|
||||
if (OSCompareAndSwap(0, 1, &failed_log_queue_requests_)) {
|
||||
LOGW("Dropping log queue messages");
|
||||
}
|
||||
// If enqueue failed, pop an item off the queue and try again.
|
||||
uint32_t dataSize = sizeof(santa_message_t);
|
||||
uint32_t dataSize = 0;
|
||||
log_dataqueue_->dequeue(0, &dataSize);
|
||||
kr = log_dataqueue_->enqueue(message, sizeof(santa_message_t));
|
||||
} else {
|
||||
@@ -414,23 +343,6 @@ bool SantaDecisionManager::PostToLogQueue(santa_message_t *message) {
|
||||
return kr;
|
||||
}
|
||||
|
||||
uint64_t SantaDecisionManager::GetVnodeIDForVnode(
|
||||
const vfs_context_t ctx, const vnode_t vp) const {
|
||||
struct vnode_attr vap;
|
||||
VATTR_INIT(&vap);
|
||||
VATTR_WANTED(&vap, va_fsid);
|
||||
VATTR_WANTED(&vap, va_fileid);
|
||||
vnode_getattr(vp, &vap, ctx);
|
||||
return (((uint64_t)vap.va_fsid << 32) | vap.va_fileid);
|
||||
}
|
||||
|
||||
uint64_t SantaDecisionManager::GetCurrentUptime() {
|
||||
clock_sec_t sec;
|
||||
clock_usec_t usec;
|
||||
clock_get_system_microtime(&sec, &usec);
|
||||
return (uint64_t)((sec * 1000000) + usec);
|
||||
}
|
||||
|
||||
#pragma mark Invocation Tracking & PID comparison
|
||||
|
||||
void SantaDecisionManager::IncrementListenerInvocations() {
|
||||
@@ -451,32 +363,30 @@ int SantaDecisionManager::VnodeCallback(const kauth_cred_t cred,
|
||||
if (vnode_vtype(vp) != VREG) return KAUTH_RESULT_DEFER;
|
||||
|
||||
// Get ID for the vnode and convert it to a string.
|
||||
uint64_t vnode_id = GetVnodeIDForVnode(ctx, vp);
|
||||
auto 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);
|
||||
auto 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);
|
||||
RemoveFromCache(vnode_id);
|
||||
returnedAction = ACTION_RESPOND_DENY;
|
||||
}
|
||||
|
||||
switch (returnedAction) {
|
||||
case ACTION_RESPOND_ALLOW: {
|
||||
proc_t proc = vfs_context_proc(ctx);
|
||||
auto proc = vfs_context_proc(ctx);
|
||||
if (proc) {
|
||||
SantaPIDAndPPID *pidWrapper = new SantaPIDAndPPID;
|
||||
pidWrapper->pid = proc_pid(proc);
|
||||
pidWrapper->ppid = proc_ppid(proc);
|
||||
lck_rw_lock_exclusive(vnode_pid_map_lock_);
|
||||
vnode_pid_map_->setObject(vnode_str, pidWrapper);
|
||||
lck_rw_unlock_exclusive(vnode_pid_map_lock_);
|
||||
pidWrapper->release();
|
||||
pid_t pid = proc_pid(proc);
|
||||
pid_t ppid = proc_ppid(proc);
|
||||
// pid_t is 32-bit; pid is in upper 32 bits, ppid in lower.
|
||||
uint64_t val = ((uint64_t)pid << 32) | (ppid & 0xFFFFFFFF);
|
||||
vnode_pid_map_->set(vnode_id, val);
|
||||
}
|
||||
return KAUTH_RESULT_ALLOW;
|
||||
}
|
||||
@@ -495,41 +405,37 @@ void SantaDecisionManager::FileOpCallback(
|
||||
const kauth_action_t action, const vnode_t vp,
|
||||
const char *path, const char *new_path) {
|
||||
if (vp) {
|
||||
vfs_context_t context = vfs_context_create(nullptr);
|
||||
uint64_t vnode_id = GetVnodeIDForVnode(context, vp);
|
||||
auto context = vfs_context_create(nullptr);
|
||||
auto vnode_id = GetVnodeIDForVnode(context, vp);
|
||||
vfs_context_rele(context);
|
||||
|
||||
if (action == KAUTH_FILEOP_CLOSE) {
|
||||
char vnode_id_str[MAX_VNODE_ID_STR];
|
||||
snprintf(vnode_id_str, MAX_VNODE_ID_STR, "%llu", vnode_id);
|
||||
CacheCheck(vnode_id_str);
|
||||
RemoveFromCache(vnode_id);
|
||||
} else if (action == KAUTH_FILEOP_EXEC) {
|
||||
santa_message_t *message = NewMessage();
|
||||
auto message = NewMessage(nullptr);
|
||||
message->vnode_id = vnode_id;
|
||||
message->action = ACTION_NOTIFY_EXEC;
|
||||
strlcpy(message->path, path, sizeof(message->path));
|
||||
|
||||
char vnode_str[MAX_VNODE_ID_STR];
|
||||
snprintf(vnode_str, MAX_VNODE_ID_STR, "%llu", vnode_id);
|
||||
|
||||
lck_rw_lock_shared(vnode_pid_map_lock_);
|
||||
SantaPIDAndPPID *pidWrapper = OSDynamicCast(
|
||||
SantaPIDAndPPID, vnode_pid_map_->getObject(vnode_str));
|
||||
if (pidWrapper) {
|
||||
message->pid = pidWrapper->pid;
|
||||
message->ppid = pidWrapper->ppid;
|
||||
uint64_t val = vnode_pid_map_->get(vnode_id);
|
||||
if (val) {
|
||||
// pid_t is 32-bit, so pid is in upper 32 bits, ppid in lower.
|
||||
message->pid = (val >> 32);
|
||||
message->ppid = (val & ~0xFFFFFFFF00000000);
|
||||
}
|
||||
lck_rw_unlock_shared(vnode_pid_map_lock_);
|
||||
|
||||
PostToLogQueue(message);
|
||||
delete message;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out modifications to locations that are definitely not useful.
|
||||
if (ClientConnected() && !strprefix(path, "/.") && !strprefix(path, "/dev")) {
|
||||
santa_message_t *message = NewMessage();
|
||||
// Filter out modifications to locations that are definitely
|
||||
// not useful or made by santad.
|
||||
if (proc_selfpid() != client_pid_ &&
|
||||
!strprefix(path, "/.") &&
|
||||
!strprefix(path, "/dev")) {
|
||||
auto message = NewMessage(nullptr);
|
||||
strlcpy(message->path, path, sizeof(message->path));
|
||||
if (new_path) strlcpy(message->newpath, new_path, sizeof(message->newpath));
|
||||
proc_name(message->pid, message->pname, sizeof(message->pname));
|
||||
@@ -550,7 +456,9 @@ void SantaDecisionManager::FileOpCallback(
|
||||
case KAUTH_FILEOP_DELETE:
|
||||
message->action = ACTION_NOTIFY_DELETE;
|
||||
break;
|
||||
default: delete message; return;
|
||||
default:
|
||||
delete message;
|
||||
return;
|
||||
}
|
||||
|
||||
PostToLogQueue(message);
|
||||
@@ -563,7 +471,7 @@ void SantaDecisionManager::FileOpCallback(
|
||||
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) {
|
||||
SantaDecisionManager *sdm = OSDynamicCast(
|
||||
auto sdm = OSDynamicCast(
|
||||
SantaDecisionManager, reinterpret_cast<OSObject *>(idata));
|
||||
|
||||
vnode_t vp = nullptr;
|
||||
@@ -571,9 +479,6 @@ extern "C" int fileop_scope_callback(
|
||||
char *new_path = nullptr;
|
||||
|
||||
switch (action) {
|
||||
case KAUTH_FILEOP_CLOSE:
|
||||
if (!(arg2 & KAUTH_FILEOP_CLOSE_MODIFIED)) return KAUTH_RESULT_DEFER;
|
||||
// Intentional fall-through
|
||||
case KAUTH_FILEOP_DELETE:
|
||||
case KAUTH_FILEOP_EXEC:
|
||||
vp = reinterpret_cast<vnode_t>(arg0);
|
||||
@@ -600,20 +505,31 @@ 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) {
|
||||
if (action & KAUTH_VNODE_ACCESS ||
|
||||
!(action & KAUTH_VNODE_EXECUTE) ||
|
||||
idata == nullptr) {
|
||||
if (action & KAUTH_VNODE_ACCESS || idata == nullptr) {
|
||||
return KAUTH_RESULT_DEFER;
|
||||
}
|
||||
|
||||
SantaDecisionManager *sdm =
|
||||
OSDynamicCast(SantaDecisionManager, reinterpret_cast<OSObject *>(idata));
|
||||
auto sdm = OSDynamicCast(
|
||||
SantaDecisionManager, reinterpret_cast<OSObject *>(idata));
|
||||
|
||||
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;
|
||||
if (action & KAUTH_VNODE_EXECUTE) {
|
||||
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;
|
||||
} else if (action & KAUTH_VNODE_WRITE_DATA) {
|
||||
vnode_t vp = reinterpret_cast<vnode_t>(arg1);
|
||||
if (vnode_vtype(vp) != VREG) return KAUTH_RESULT_DEFER;
|
||||
sdm->IncrementListenerInvocations();
|
||||
char path[MAXPATHLEN];
|
||||
int pathlen = MAXPATHLEN;
|
||||
vn_getpath(vp, path, &pathlen);
|
||||
sdm->FileOpCallback(KAUTH_FILEOP_CLOSE, vp, path, nullptr);
|
||||
sdm->DecrementListenerInvocations();
|
||||
}
|
||||
|
||||
return KAUTH_RESULT_DEFER;
|
||||
}
|
||||
|
||||
@@ -24,10 +24,9 @@
|
||||
#include <sys/proc.h>
|
||||
#include <sys/vnode.h>
|
||||
|
||||
#include "SantaCache.h"
|
||||
#include "SNTKernelCommon.h"
|
||||
#include "SNTLogging.h"
|
||||
#include "SantaCachedDecision.h"
|
||||
#include "SantaPIDAndPPID.h"
|
||||
|
||||
///
|
||||
/// SantaDecisionManager is responsible for intercepting Vnode execute actions
|
||||
@@ -40,207 +39,227 @@ class SantaDecisionManager : public OSObject {
|
||||
OSDeclareDefaultStructors(SantaDecisionManager);
|
||||
|
||||
public:
|
||||
/// Used for initialization after instantiation. Required because
|
||||
/// constructors cannot throw inside kernel-space.
|
||||
/// Used for initialization after instantiation.
|
||||
bool init() override;
|
||||
|
||||
/// Called automatically when retain count drops to 0.
|
||||
/// Called automatically when retain count drops to 0.
|
||||
void free() override;
|
||||
|
||||
/// Called by SantaDriverClient during connection to provide the shared
|
||||
/// dataqueue memory to the client.
|
||||
/**
|
||||
Called by SantaDriverClient during connection to provide the shared
|
||||
dataqueue memory to the client for the decision queue.
|
||||
*/
|
||||
IOMemoryDescriptor *GetDecisionMemoryDescriptor() const;
|
||||
|
||||
/**
|
||||
Called by SantaDriverClient during connection to provide the shared
|
||||
dataqueue memory to the client for the logging queue.
|
||||
*/
|
||||
IOMemoryDescriptor *GetLogMemoryDescriptor() const;
|
||||
|
||||
/// Called by SantaDriverClient when a client connects to the decision queue,
|
||||
/// providing the pid of the client process.
|
||||
/**
|
||||
Called by SantaDriverClient when a client connects to the decision queue,
|
||||
providing the pid of the client process.
|
||||
*/
|
||||
void ConnectClient(pid_t pid);
|
||||
|
||||
/// Called by SantaDriverClient when a client disconnects
|
||||
/// Called by SantaDriverClient when a client disconnects
|
||||
void DisconnectClient(bool itDied = false);
|
||||
|
||||
/// Returns whether a client is currently connected or not.
|
||||
/// Returns whether a client is currently connected or not.
|
||||
bool ClientConnected() const;
|
||||
|
||||
/// Sets the Mach port for notifying the decision queue.
|
||||
/// Sets the Mach port for notifying the decision queue.
|
||||
void SetDecisionPort(mach_port_t port);
|
||||
|
||||
/// Sets the Mach port for notifying the log queue.
|
||||
/// Sets the Mach port for notifying the log queue.
|
||||
void SetLogPort(mach_port_t port);
|
||||
|
||||
/// Starts the kauth listeners.
|
||||
/// Starts the kauth listeners.
|
||||
kern_return_t StartListener();
|
||||
|
||||
/// Stops the kauth listeners. After stopping new callback requests,
|
||||
/// waits until all current invocations have finished before clearing the
|
||||
/// cache and returning.
|
||||
/**
|
||||
Stops the kauth listeners. After stopping new callback requests, waits until all
|
||||
current invocations have finished before clearing the cache and returning.
|
||||
*/
|
||||
kern_return_t StopListener();
|
||||
|
||||
/// Adds a decision to the cache, with a timestamp.
|
||||
void AddToCache(const char *identifier,
|
||||
/// Adds a decision to the cache, with a timestamp.
|
||||
void AddToCache(uint64_t identifier,
|
||||
const santa_action_t decision,
|
||||
const uint64_t microsecs = GetCurrentUptime());
|
||||
|
||||
/// Checks to see if a given identifier is in the cache and removes it.
|
||||
void CacheCheck(const char *identifier);
|
||||
/// Fetches a response from the cache, first checking to see if the entry has expired.
|
||||
santa_action_t GetFromCache(uint64_t identifier);
|
||||
|
||||
/// Returns the number of entries in the cache.
|
||||
/// Checks to see if a given identifier is in the cache and removes it.
|
||||
void RemoveFromCache(uint64_t identifier);
|
||||
|
||||
/// Returns the number of entries in the cache.
|
||||
uint64_t CacheCount() const;
|
||||
|
||||
/// Clears the cache.
|
||||
/// Clears the cache.
|
||||
void ClearCache();
|
||||
|
||||
/// Increments the count of active callbacks pending.
|
||||
/// Increments the count of active callbacks pending.
|
||||
void IncrementListenerInvocations();
|
||||
|
||||
/// Decrements the count of active callbacks pending.
|
||||
/// Decrements the count of active callbacks 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_*.
|
||||
///
|
||||
/**
|
||||
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 action The performed action
|
||||
/// @param vp The Vnode for this request. May be nullptr.
|
||||
/// @param path The path being operated on.
|
||||
/// @param new_path The target path for moves and links.
|
||||
///
|
||||
/**
|
||||
FileOp Callback
|
||||
|
||||
@param action The performed action
|
||||
@param vp The Vnode for this request. May be nullptr.
|
||||
@param path The path being operated on.
|
||||
@param new_path The target path for moves and links.
|
||||
*/
|
||||
void FileOpCallback(kauth_action_t action, const vnode_t vp,
|
||||
const char *path, const char *new_path);
|
||||
|
||||
protected:
|
||||
///
|
||||
/// The maximum number of milliseconds a cached deny message should be
|
||||
/// considered valid.
|
||||
///
|
||||
const uint64_t kMaxDenyCacheTimeMilliseconds = 500;
|
||||
/**
|
||||
While waiting for a response from the daemon, this is the number of
|
||||
milliseconds to sleep for before checking the cache for a response.
|
||||
*/
|
||||
static const uint32_t kRequestLoopSleepMilliseconds = 10;
|
||||
|
||||
///
|
||||
/// The maximum number of milliseconds a cached allow message should be
|
||||
/// considered valid.
|
||||
///
|
||||
const uint64_t kMaxAllowCacheTimeMilliseconds = 1000 * 60 * 60 * 24;
|
||||
/// The maximum number of milliseconds a cached deny message should be considered valid.
|
||||
static const uint64_t kMaxDenyCacheTimeMilliseconds = 500;
|
||||
|
||||
///
|
||||
/// While waiting for a response from the daemon, this is the number of
|
||||
/// milliseconds to sleep for before checking the cache for a response.
|
||||
///
|
||||
const int kRequestLoopSleepMilliseconds = 10;
|
||||
/// Maximum number of entries in the in-kernel cache.
|
||||
static const uint32_t kMaxCacheSize = 10000;
|
||||
|
||||
///
|
||||
/// Maximum number of entries in the in-kernel cache.
|
||||
///
|
||||
const int kMaxCacheSize = 10000;
|
||||
/// Maximum number of PostToDecisionQueue failures to allow.
|
||||
static const uint32_t kMaxDecisionQueueFailures = 10;
|
||||
|
||||
///
|
||||
/// Maximum number of PostToDecisionQueue failures to allow.
|
||||
///
|
||||
const int kMaxDecisionQueueFailures = 10;
|
||||
/// The maximum number of messages can be kept in the decision data queue at any time.
|
||||
static const uint32_t kMaxDecisionQueueEvents = 512;
|
||||
|
||||
///
|
||||
/// The maximum number of messages can be kept in
|
||||
/// the decision data queue at any time.
|
||||
///
|
||||
const int kMaxDecisionQueueEvents = 512;
|
||||
/// The maximum number of messages can be kept in the logging data queue at any time.
|
||||
static const uint32_t kMaxLogQueueEvents = 1024;
|
||||
|
||||
///
|
||||
/// The maximum number of messages can be kept
|
||||
/// in the logging data queue at any time.
|
||||
///
|
||||
const int kMaxLogQueueEvents = 1024;
|
||||
/**
|
||||
Fetches a response from the daemon. Handles both daemon death
|
||||
and failure to post messages to the daemon.
|
||||
|
||||
/// Fetches a response from the cache, first checking to see if the
|
||||
/// entry has expired.
|
||||
santa_action_t GetFromCache(const char *identifier);
|
||||
@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(santa_message_t *message, uint64_t 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(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.
|
||||
|
||||
///
|
||||
/// 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);
|
||||
@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 decision data queue.
|
||||
///
|
||||
/// @param message The message to send
|
||||
/// @return bool true if sending was successful.
|
||||
///
|
||||
/**
|
||||
Posts the requested message to the decision data queue.
|
||||
|
||||
@param message The message to send
|
||||
@return bool true if sending was successful.
|
||||
*/
|
||||
bool PostToDecisionQueue(santa_message_t *message);
|
||||
|
||||
///
|
||||
/// Posts the requested message to the logging data queue.
|
||||
///
|
||||
/// @param message The message to send
|
||||
/// @return bool true if sending was successful.
|
||||
///
|
||||
/**
|
||||
Posts the requested message to the logging data queue.
|
||||
|
||||
@param message The message to send
|
||||
@return bool true if sending was successful.
|
||||
*/
|
||||
bool PostToLogQueue(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) const;
|
||||
/**
|
||||
Fetches the vnode_id for a given vnode.
|
||||
|
||||
///
|
||||
/// Creates a new santa_message_t with some fields pre-filled.
|
||||
///
|
||||
santa_message_t *NewMessage() const;
|
||||
@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.
|
||||
*/
|
||||
static inline uint64_t GetVnodeIDForVnode(const vfs_context_t ctx, const vnode_t vp) {
|
||||
struct vnode_attr vap;
|
||||
VATTR_INIT(&vap);
|
||||
VATTR_WANTED(&vap, va_fsid);
|
||||
VATTR_WANTED(&vap, va_fileid);
|
||||
vnode_getattr(vp, &vap, ctx);
|
||||
return (((uint64_t)vap.va_fsid << 32) | vap.va_fileid);
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns the current system uptime in microseconds
|
||||
///
|
||||
static uint64_t GetCurrentUptime();
|
||||
/**
|
||||
Creates a new santa_message_t with some fields pre-filled.
|
||||
|
||||
@param credential The kauth_cred_t for this action, if available.
|
||||
If nullptr, will get the credential for the current process.
|
||||
*/
|
||||
static inline santa_message_t *NewMessage(kauth_cred_t credential) {
|
||||
bool should_release = false;
|
||||
if (credential == nullptr) {
|
||||
credential = kauth_cred_get_with_ref();
|
||||
should_release = true;
|
||||
}
|
||||
|
||||
auto message = new santa_message_t;
|
||||
message->uid = kauth_cred_getuid(credential);
|
||||
message->gid = kauth_cred_getgid(credential);
|
||||
message->pid = proc_selfpid();
|
||||
message->ppid = proc_selfppid();
|
||||
|
||||
if (should_release) {
|
||||
kauth_cred_unref(&credential);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the current system uptime in microseconds
|
||||
*/
|
||||
static inline uint64_t GetCurrentUptime() {
|
||||
clock_sec_t sec;
|
||||
clock_usec_t usec;
|
||||
clock_get_system_microtime(&sec, &usec);
|
||||
return (uint64_t)((sec * 1000000) + usec);
|
||||
}
|
||||
|
||||
private:
|
||||
SantaCache<uint64_t> *decision_cache_;
|
||||
SantaCache<uint64_t> *vnode_pid_map_;
|
||||
|
||||
lck_grp_t *sdm_lock_grp_;
|
||||
lck_grp_attr_t *sdm_lock_grp_attr_;
|
||||
|
||||
lck_attr_t *sdm_lock_attr_;
|
||||
lck_rw_t *cached_decisions_lock_;
|
||||
|
||||
lck_mtx_t *decision_dataqueue_lock_;
|
||||
lck_mtx_t *log_dataqueue_lock_;
|
||||
lck_rw_t *vnode_pid_map_lock_;
|
||||
|
||||
OSDictionary *cached_decisions_;
|
||||
OSDictionary *vnode_pid_map_;
|
||||
|
||||
IOSharedDataQueue *decision_dataqueue_;
|
||||
IOSharedDataQueue *log_dataqueue_;
|
||||
SInt32 failed_decision_queue_requests_;
|
||||
SInt32 failed_log_queue_requests_;
|
||||
int32_t failed_decision_queue_requests_;
|
||||
int32_t failed_log_queue_requests_;
|
||||
|
||||
SInt32 listener_invocations_;
|
||||
int32_t listener_invocations_;
|
||||
|
||||
pid_t client_pid_;
|
||||
|
||||
@@ -248,32 +267,44 @@ class SantaDecisionManager : public OSObject {
|
||||
kauth_listener_t fileop_listener_;
|
||||
};
|
||||
|
||||
///
|
||||
/// The kauth callback function for the Vnode scope
|
||||
/// @param actor's credentials
|
||||
/// @param data that was passed when the listener was registered
|
||||
/// @param action that was requested
|
||||
/// @param VFS context
|
||||
/// @param Vnode being operated on
|
||||
/// @param Parent Vnode. May be nullptr.
|
||||
/// @param Pointer to an errno-style error.
|
||||
///
|
||||
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 kauth callback function for the Vnode scope
|
||||
|
||||
///
|
||||
/// The kauth callback function for the FileOp scope
|
||||
/// @param actor's credentials
|
||||
/// @param data that was passed when the listener was registered
|
||||
/// @param action that was requested
|
||||
/// @param depends on action, usually the vnode ref.
|
||||
/// @param depends on action.
|
||||
/// @param depends on action, usually 0.
|
||||
/// @param depends on action, usually 0.
|
||||
///
|
||||
@param actor's credentials
|
||||
@param data that was passed when the listener was registered
|
||||
@param action that was requested
|
||||
@param VFS context
|
||||
@param Vnode being operated on
|
||||
@param Parent Vnode. May be nullptr.
|
||||
@param Pointer to an errno-style error.
|
||||
*/
|
||||
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 kauth callback function for the FileOp scope
|
||||
|
||||
@param actor's credentials
|
||||
@param data that was passed when the listener was registered
|
||||
@param action that was requested
|
||||
@param depends on action, usually the vnode ref.
|
||||
@param depends on action.
|
||||
@param depends on action, usually 0.
|
||||
@param depends on action, usually 0.
|
||||
*/
|
||||
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);
|
||||
kauth_cred_t credential,
|
||||
void *idata,
|
||||
kauth_action_t action,
|
||||
uintptr_t arg0,
|
||||
uintptr_t arg1,
|
||||
uintptr_t arg2,
|
||||
uintptr_t arg3);
|
||||
|
||||
#endif // SANTA__SANTA_DRIVER__SANTADECISIONMANAGER_H
|
||||
|
||||
@@ -130,10 +130,7 @@ IOReturn SantaDriverClient::static_open(
|
||||
}
|
||||
|
||||
IOReturn SantaDriverClient::allow_binary(const uint64_t vnode_id) {
|
||||
char vnode_id_str[21];
|
||||
snprintf(vnode_id_str, sizeof(vnode_id_str), "%llu", vnode_id);
|
||||
decisionManager->AddToCache(vnode_id_str, ACTION_RESPOND_ALLOW);
|
||||
|
||||
decisionManager->AddToCache(vnode_id, ACTION_RESPOND_ALLOW);
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
|
||||
@@ -149,10 +146,7 @@ IOReturn SantaDriverClient::static_allow_binary(
|
||||
}
|
||||
|
||||
IOReturn SantaDriverClient::deny_binary(const uint64_t vnode_id) {
|
||||
char vnode_id_str[21];
|
||||
snprintf(vnode_id_str, sizeof(vnode_id_str), "%llu", vnode_id);
|
||||
decisionManager->AddToCache(vnode_id_str, ACTION_RESPOND_DENY);
|
||||
|
||||
decisionManager->AddToCache(vnode_id, ACTION_RESPOND_DENY);
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
|
||||
@@ -193,6 +187,20 @@ IOReturn SantaDriverClient::static_cache_count(
|
||||
return target->cache_count(&(arguments->scalarOutput[0]));
|
||||
}
|
||||
|
||||
IOReturn SantaDriverClient::check_cache(uint64_t vnode_id, uint64_t *output) {
|
||||
*output = decisionManager->GetFromCache(vnode_id);
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
|
||||
IOReturn SantaDriverClient::static_check_cache(
|
||||
com_google_SantaDriverClient *target,
|
||||
void *reference,
|
||||
IOExternalMethodArguments *arguments) {
|
||||
if (!target) return kIOReturnBadArgument;
|
||||
return target->check_cache(reinterpret_cast<uint64_t>(*arguments->scalarInput),
|
||||
&(arguments->scalarOutput[0]));
|
||||
}
|
||||
|
||||
#pragma mark Method Resolution
|
||||
|
||||
IOReturn SantaDriverClient::externalMethod(
|
||||
@@ -203,7 +211,7 @@ IOReturn SantaDriverClient::externalMethod(
|
||||
void *reference) {
|
||||
/// Array of methods callable by clients. The order of these must match the
|
||||
/// order of the items in SantaDriverMethods in SNTKernelCommon.h
|
||||
IOExternalMethodDispatch sMethods[kSantaUserClientNMethods] = {
|
||||
static IOExternalMethodDispatch sMethods[kSantaUserClientNMethods] = {
|
||||
{
|
||||
reinterpret_cast<IOExternalMethodAction>(&SantaDriverClient::static_open),
|
||||
0, // input scalar
|
||||
@@ -242,6 +250,14 @@ IOReturn SantaDriverClient::externalMethod(
|
||||
0,
|
||||
1,
|
||||
0
|
||||
},
|
||||
{
|
||||
reinterpret_cast<IOExternalMethodAction>(
|
||||
&SantaDriverClient::static_check_cache),
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -111,6 +111,14 @@ class com_google_SantaDriverClient : public IOUserClient {
|
||||
void *reference,
|
||||
IOExternalMethodArguments *arguments);
|
||||
|
||||
/// The daemon calls this to find out the status of a vnode_id in the cache.
|
||||
/// Output will be a santa_action_t.
|
||||
IOReturn check_cache(uint64_t vnode_id, uint64_t *output);
|
||||
static IOReturn static_check_cache(
|
||||
com_google_SantaDriverClient *target,
|
||||
void *reference,
|
||||
IOExternalMethodArguments *arguments);
|
||||
|
||||
private:
|
||||
com_google_SantaDriver *myProvider;
|
||||
SantaDecisionManager *decisionManager;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTA_DRIVER__SANTAPIDANDPPID_H
|
||||
#define SANTA__SANTA_DRIVER__SANTAPIDANDPPID_H
|
||||
|
||||
#include <libkern/c++/OSObject.h>
|
||||
|
||||
///
|
||||
/// An OSObject wrapper around a PID and PPID.
|
||||
/// Only OSObject subclasses can be inserted into an OSDictionary.
|
||||
///
|
||||
class SantaPIDAndPPID : public OSObject {
|
||||
OSDeclareDefaultStructors(SantaPIDAndPPID)
|
||||
|
||||
public:
|
||||
pid_t pid;
|
||||
pid_t ppid;
|
||||
};
|
||||
|
||||
#endif // SANTA__SANTA_DRIVER__SANTAPIDANDPPID_H
|
||||
71
Source/santactl/Commands/SNTCommandCheckCache.m
Normal file
71
Source/santactl/Commands/SNTCommandCheckCache.m
Normal file
@@ -0,0 +1,71 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommandController.h"
|
||||
|
||||
#import "SNTLogging.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
@interface SNTCommandCheckCache : NSObject<SNTCommand>
|
||||
@end
|
||||
|
||||
@implementation SNTCommandCheckCache
|
||||
|
||||
#ifdef DEBUG
|
||||
REGISTER_COMMAND_NAME(@"checkcache")
|
||||
#endif
|
||||
|
||||
+ (BOOL)requiresRoot {
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)requiresDaemonConn {
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (NSString *)shortHelpText {
|
||||
return @"Prints the status of a file in the kernel cache.";
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
return (@"Checks the in-kernel cache for desired file.\n"
|
||||
@"Returns 0 if successful, 1 otherwise");
|
||||
}
|
||||
|
||||
+ (void)runWithArguments:(NSArray *)arguments daemonConnection:(SNTXPCConnection *)daemonConn {
|
||||
uint64_t vnodeID = [self vnodeIDForFile:arguments.firstObject];
|
||||
[[daemonConn remoteObjectProxy] checkCacheForVnodeID:vnodeID withReply:^(santa_action_t action) {
|
||||
if (action == ACTION_RESPOND_ALLOW) {
|
||||
LOGI(@"File exists in [whitelist] kernel cache");
|
||||
exit(0);
|
||||
} else if (action == ACTION_RESPOND_DENY) {
|
||||
LOGI(@"File exists in [blacklist] kernel cache");
|
||||
exit(0);
|
||||
} else if (action == ACTION_UNSET) {
|
||||
LOGE(@"File does not exist in cache");
|
||||
exit(1);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
+ (uint64_t)vnodeIDForFile:(NSString *)path {
|
||||
struct stat fstat = {};
|
||||
stat(path.fileSystemRepresentation, &fstat);
|
||||
return (((uint64_t)fstat.st_dev << 32) | fstat.st_ino);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -19,6 +19,9 @@
|
||||
#import "MOLCertificate.h"
|
||||
#import "MOLCodesignChecker.h"
|
||||
#import "SNTFileInfo.h"
|
||||
#import "SNTRule.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@interface SNTCommandFileInfo : NSObject<SNTCommand>
|
||||
@end
|
||||
@@ -100,11 +103,10 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
[self printKey:@"Page Zero" value:@"__PAGEZERO segment missing/bad!"];
|
||||
}
|
||||
|
||||
// Code signature state
|
||||
NSError *error;
|
||||
MOLCodesignChecker *csc = [[MOLCodesignChecker alloc] initWithBinaryPath:filePath error:&error];
|
||||
if (!error) {
|
||||
[self printKey:@"Code-signed" value:@"Yes"];
|
||||
} else {
|
||||
if (error) {
|
||||
switch (error.code) {
|
||||
case errSecCSUnsigned:
|
||||
[self printKey:@"Code-signed" value:@"No"];
|
||||
@@ -113,7 +115,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
case errSecCSStaticCodeChanged:
|
||||
case errSecCSSignatureNotVerifiable:
|
||||
case errSecCSSignatureUnsupported:
|
||||
[self printKey:@"Code-signed" value:@"Yes, but code/signatured changed/unverifiable"];
|
||||
[self printKey:@"Code-signed" value:@"Yes, but code/signature changed/unverifiable"];
|
||||
break;
|
||||
case errSecCSResourceDirectoryFailed:
|
||||
case errSecCSResourceNotSupported:
|
||||
@@ -133,13 +135,64 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
break;
|
||||
default: {
|
||||
NSString *val = [NSString stringWithFormat:@"Yes, but failed to validate (%ld)",
|
||||
error.code];
|
||||
error.code];
|
||||
[self printKey:@"Code-signed" value:val];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (csc.signatureFlags & kSecCodeSignatureAdhoc) {
|
||||
[self printKey:@"Code-signed" value:@"Yes, but ad-hoc"];
|
||||
} else {
|
||||
[self printKey:@"Code-signed" value:@"Yes"];
|
||||
}
|
||||
if (csc.certificates) {
|
||||
|
||||
// Binary rule state
|
||||
__block SNTRule *r;
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
[daemonConn resume];
|
||||
dispatch_group_enter(group);
|
||||
[[daemonConn remoteObjectProxy] databaseBinaryRuleForSHA256:sha256 reply:^(SNTRule *rule) {
|
||||
if (rule) r = rule;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
NSString *leafCertSHA = [[csc.certificates firstObject] SHA256];
|
||||
dispatch_group_enter(group);
|
||||
[[daemonConn remoteObjectProxy] databaseCertificateRuleForSHA256:leafCertSHA
|
||||
reply:^(SNTRule *rule) {
|
||||
if (!r && rule) r = rule;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
[self printKey:@"Rule" value:@"Cannot communicate with daemon"];
|
||||
} else {
|
||||
NSString *output;
|
||||
switch (r.state) {
|
||||
case SNTRuleStateWhitelist:
|
||||
output = @"Whitelisted";
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
output = @"\033[32mWhitelisted\033[0m";
|
||||
}
|
||||
[self printKey:@"Rule" value:output];
|
||||
break;
|
||||
case SNTRuleStateBlacklist:
|
||||
case SNTRuleStateSilentBlacklist:
|
||||
output = @"Blacklisted";
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
output = @"\033[31mBlacklisted\033[0m";
|
||||
}
|
||||
[self printKey:@"Rule" value:output];
|
||||
break;
|
||||
default:
|
||||
output = @"None";
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
output = @"\033[33mNone\033[0m";
|
||||
}
|
||||
[self printKey:@"Rule" value:output];
|
||||
}
|
||||
}
|
||||
|
||||
// Signing chain
|
||||
if (csc.certificates.count) {
|
||||
printf("Signing chain:\n");
|
||||
|
||||
[csc.certificates enumerateObjectsUsingBlock:^(MOLCertificate *c,
|
||||
@@ -168,11 +221,10 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
|
||||
+ (NSString *)humanReadableFileType:(SNTFileInfo *)fi {
|
||||
if ([fi isScript]) return @"Script";
|
||||
if ([fi isXARArchive]) return @"XAR Archive";
|
||||
if ([fi isExecutable]) return @"Executable";
|
||||
if ([fi isDylib]) return @"Dynamic Library";
|
||||
if ([fi isKext]) return @"Kernel Extension";
|
||||
if ([fi isFat]) return @"Fat Binary";
|
||||
if ([fi isMachO]) return @"Thin Binary";
|
||||
if ([fi isXARArchive]) return @"XAR Archive";
|
||||
if ([fi isDMG]) return @"Disk Image";
|
||||
return @"Unknown";
|
||||
}
|
||||
@@ -23,7 +23,9 @@
|
||||
|
||||
@implementation SNTCommandFlushCache
|
||||
|
||||
#ifdef DEBUG
|
||||
REGISTER_COMMAND_NAME(@"flushcache")
|
||||
#endif
|
||||
|
||||
+ (BOOL)requiresRoot {
|
||||
return YES;
|
||||
@@ -77,8 +77,8 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
|
||||
SNTRule *newRule = [[SNTRule alloc] init];
|
||||
newRule.state = RULESTATE_UNKNOWN;
|
||||
newRule.type = RULETYPE_BINARY;
|
||||
newRule.state = SNTRuleStateUnknown;
|
||||
newRule.type = SNTRuleTypeBinary;
|
||||
|
||||
NSString *path;
|
||||
|
||||
@@ -87,15 +87,15 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
NSString *arg = arguments[i];
|
||||
|
||||
if ([arg caseInsensitiveCompare:@"--whitelist"] == NSOrderedSame) {
|
||||
newRule.state = RULESTATE_WHITELIST;
|
||||
newRule.state = SNTRuleStateWhitelist;
|
||||
} else if ([arg caseInsensitiveCompare:@"--blacklist"] == NSOrderedSame) {
|
||||
newRule.state = RULESTATE_BLACKLIST;
|
||||
newRule.state = SNTRuleStateBlacklist;
|
||||
} else if ([arg caseInsensitiveCompare:@"--silent-blacklist"] == NSOrderedSame) {
|
||||
newRule.state = RULESTATE_SILENT_BLACKLIST;
|
||||
newRule.state = SNTRuleStateSilentBlacklist;
|
||||
} else if ([arg caseInsensitiveCompare:@"--remove"] == NSOrderedSame) {
|
||||
newRule.state = RULESTATE_REMOVE;
|
||||
newRule.state = SNTRuleStateRemove;
|
||||
} else if ([arg caseInsensitiveCompare:@"--certificate"] == NSOrderedSame) {
|
||||
newRule.type = RULETYPE_CERT;
|
||||
newRule.type = SNTRuleTypeCertificate;
|
||||
} else if ([arg caseInsensitiveCompare:@"--path"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--path requires an argument"];
|
||||
@@ -121,26 +121,29 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
|
||||
if (path) {
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:path];
|
||||
if (newRule.type == RULETYPE_BINARY) {
|
||||
if (newRule.type == SNTRuleTypeBinary) {
|
||||
newRule.shasum = fi.SHA256;
|
||||
} else if (newRule.type == RULETYPE_CERT) {
|
||||
} else if (newRule.type == SNTRuleTypeCertificate) {
|
||||
MOLCodesignChecker *cs = [[MOLCodesignChecker alloc] initWithBinaryPath:fi.path];
|
||||
newRule.shasum = cs.leafCertificate.SHA256;
|
||||
}
|
||||
}
|
||||
|
||||
if (newRule.state == RULESTATE_UNKNOWN) {
|
||||
if (newRule.state == SNTRuleStateUnknown) {
|
||||
[self printErrorUsageAndExit:@"No state specified"];
|
||||
} else if (!newRule.shasum) {
|
||||
[self printErrorUsageAndExit:@"Either SHA-256 or path to file must be specified"];
|
||||
}
|
||||
|
||||
[[daemonConn remoteObjectProxy] databaseRuleAddRule:newRule cleanSlate:NO reply:^(BOOL success) {
|
||||
if (!success) {
|
||||
printf("Failed to modify rules.");
|
||||
[[daemonConn remoteObjectProxy] databaseRuleAddRules:@[newRule]
|
||||
cleanSlate:NO
|
||||
reply:^(NSError *error) {
|
||||
if (error) {
|
||||
printf("Failed to modify rules: %s", [error.localizedDescription UTF8String]);
|
||||
LOGD(@"Failure reason: %@", error.localizedFailureReason);
|
||||
exit(1);
|
||||
} else {
|
||||
if (newRule.state == RULESTATE_REMOVE) {
|
||||
if (newRule.state == SNTRuleStateRemove) {
|
||||
printf("Removed rule for SHA-256: %s.\n", [newRule.shasum UTF8String]);
|
||||
} else {
|
||||
printf("Added rule for SHA-256: %s.\n", [newRule.shasum UTF8String]);
|
||||
@@ -50,16 +50,16 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
__block uint64_t cpuEvents, ramEvents;
|
||||
__block double cpuPeak, ramPeak;
|
||||
dispatch_group_enter(group);
|
||||
[[daemonConn remoteObjectProxy] clientMode:^(santa_clientmode_t cm) {
|
||||
[[daemonConn remoteObjectProxy] clientMode:^(SNTClientMode cm) {
|
||||
switch (cm) {
|
||||
case CLIENTMODE_MONITOR:
|
||||
case SNTClientModeMonitor:
|
||||
clientMode = @"Monitor";
|
||||
break;
|
||||
case CLIENTMODE_LOCKDOWN:
|
||||
case SNTClientModeLockdown:
|
||||
clientMode = @"Lockdown";
|
||||
break;
|
||||
default:
|
||||
clientMode = [NSString stringWithFormat:@"Unknown (%d)", cm];
|
||||
clientMode = [NSString stringWithFormat:@"Unknown (%ld)", cm];
|
||||
break;
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
@@ -14,7 +14,8 @@
|
||||
|
||||
#import "SNTCommandController.h"
|
||||
|
||||
#import "SNTAuthenticatingURLSession.h"
|
||||
#import <MOLAuthenticatingURLSession.h>
|
||||
|
||||
#import "SNTCommandSyncEventUpload.h"
|
||||
#import "SNTCommandSyncLogUpload.h"
|
||||
#import "SNTCommandSyncPostflight.h"
|
||||
@@ -28,8 +29,6 @@
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@interface SNTCommandSync : NSObject<SNTCommand>
|
||||
@property NSURLSession *session;
|
||||
@property SNTXPCConnection *daemonConn;
|
||||
@property SNTCommandSyncState *syncState;
|
||||
@end
|
||||
|
||||
@@ -50,50 +49,73 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
return nil;
|
||||
return (@"If Santa is configured to synchronize with a a server, "
|
||||
@"this is the command used for syncing.\n\n"
|
||||
@"Options:\n"
|
||||
@" --clean: Perform a clean sync, erasing all existing rules and requesting a"
|
||||
@" clean sync from the server.");
|
||||
}
|
||||
|
||||
+ (void)runWithArguments:(NSArray *)arguments daemonConnection:(SNTXPCConnection *)daemonConn {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
// Ensure we have no privileges
|
||||
if (!DropRootPrivileges()) {
|
||||
LOGE(@"Failed to drop root privileges. Exiting.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
SNTCommandSync *s = [[self alloc] init];
|
||||
|
||||
// Gather some data needed during some sync stages
|
||||
s.syncState = [[SNTCommandSyncState alloc] init];
|
||||
|
||||
s.syncState.syncBaseURL = config.syncBaseURL;
|
||||
if (s.syncState.syncBaseURL.absoluteString.length == 0) {
|
||||
LOGE(@"Missing SyncBaseURL. Can't sync without it.");
|
||||
exit(1);
|
||||
} else if (![s.syncState.syncBaseURL.scheme isEqual:@"https"]) {
|
||||
LOGW(@"SyncBaseURL is not over HTTPS!");
|
||||
}
|
||||
|
||||
s.syncState.machineID = config.machineID;
|
||||
if (s.syncState.machineID.length == 0) {
|
||||
LOGE(@"Missing Machine ID. Can't sync without it.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
s.syncState.machineOwner = config.machineOwner;
|
||||
if (s.syncState.machineOwner.length == 0) {
|
||||
s.syncState.machineOwner = @"";
|
||||
LOGW(@"Missing Machine Owner.");
|
||||
}
|
||||
|
||||
[[daemonConn remoteObjectProxy] xsrfToken:^(NSString *token) {
|
||||
s.syncState.xsrfToken = token;
|
||||
}];
|
||||
|
||||
// Dropping root privileges to the 'nobody' user causes the default NSURLCache to throw
|
||||
// sandbox errors, which are benign but annoying. This line disables the cache entirely.
|
||||
[NSURLCache setSharedURLCache:[[NSURLCache alloc] initWithMemoryCapacity:0
|
||||
diskCapacity:0
|
||||
diskPath:nil]];
|
||||
|
||||
SNTCommandSync *s = [[self alloc] init];
|
||||
|
||||
SNTAuthenticatingURLSession *authURLSession = [[SNTAuthenticatingURLSession alloc] init];
|
||||
|
||||
MOLAuthenticatingURLSession *authURLSession = [[MOLAuthenticatingURLSession alloc] init];
|
||||
authURLSession.userAgent = @"santactl-sync/";
|
||||
NSString *santactlVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
||||
if (santactlVersion) {
|
||||
authURLSession.userAgent = [authURLSession.userAgent stringByAppendingString:santactlVersion];
|
||||
}
|
||||
authURLSession.refusesRedirects = YES;
|
||||
authURLSession.serverHostname = s.syncState.syncBaseURL.host;
|
||||
authURLSession.loggingBlock = ^(NSString *line) {
|
||||
LOGD(@"%@", line);
|
||||
};
|
||||
|
||||
// Configure server auth
|
||||
if ([config syncServerAuthRootsFile]) {
|
||||
NSError *error = nil;
|
||||
|
||||
NSData *rootsData = [NSData dataWithContentsOfFile:[config syncServerAuthRootsFile]
|
||||
options:0
|
||||
error:&error];
|
||||
authURLSession.serverRootsPemData = rootsData;
|
||||
|
||||
if (!rootsData) {
|
||||
LOGE(@"Couldn't open server root certificate file %@ with error: %@.",
|
||||
[config syncServerAuthRootsFile],
|
||||
[error localizedDescription]);
|
||||
exit(1);
|
||||
}
|
||||
authURLSession.serverRootsPemFile = [config syncServerAuthRootsFile];
|
||||
} else if ([config syncServerAuthRootsData]) {
|
||||
authURLSession.serverRootsPemData = [config syncServerAuthRootsData];
|
||||
}
|
||||
@@ -108,134 +130,108 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
authURLSession.clientCertIssuerCn = [config syncClientAuthCertificateIssuer];
|
||||
}
|
||||
|
||||
s.session = [authURLSession session];
|
||||
s.daemonConn = daemonConn;
|
||||
s.syncState.session = [authURLSession session];
|
||||
s.syncState.daemonConn = daemonConn;
|
||||
|
||||
// Gather some data needed during some sync stages
|
||||
s.syncState = [[SNTCommandSyncState alloc] init];
|
||||
if ([arguments containsObject:@"singleevent"]) {
|
||||
NSUInteger idx = [arguments indexOfObject:@"singleevent"] + 1;
|
||||
if (idx >= arguments.count) {
|
||||
LOGI(@"singleevent takes an argument");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
s.syncState.syncBaseURL = config.syncBaseURL;
|
||||
if (!s.syncState.syncBaseURL) {
|
||||
LOGE(@"Missing SyncBaseURL. Can't sync without it.");
|
||||
exit(1);
|
||||
} else if (![s.syncState.syncBaseURL.scheme isEqual:@"https"]) {
|
||||
LOGW(@"SyncBaseURL is not over HTTPS!");
|
||||
}
|
||||
authURLSession.serverHostname = s.syncState.syncBaseURL.host;
|
||||
|
||||
s.syncState.machineID = config.machineID;
|
||||
if ([s.syncState.machineID length] == 0) {
|
||||
LOGE(@"Missing Machine ID. Can't sync without it.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
s.syncState.machineOwner = config.machineOwner;
|
||||
if ([s.syncState.machineOwner length] == 0) {
|
||||
s.syncState.machineOwner = @"";
|
||||
LOGW(@"Missing Machine Owner.");
|
||||
}
|
||||
|
||||
if (arguments.count == 2 && [[arguments firstObject] isEqual:@"singleevent"]) {
|
||||
[s eventUploadSingleEvent:arguments[1]];
|
||||
NSString *obj = arguments[idx];
|
||||
if (obj.length != 64) {
|
||||
LOGI(@"singleevent passed without SHA-256 as next argument");
|
||||
exit(1);
|
||||
}
|
||||
return [s eventUploadSingleEvent:obj];
|
||||
} else {
|
||||
[s preflight];
|
||||
return [s preflight];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)preflight {
|
||||
[SNTCommandSyncPreflight performSyncInSession:self.session
|
||||
syncState:self.syncState
|
||||
daemonConn:self.daemonConn
|
||||
completionHandler:^(BOOL success) {
|
||||
if (success) {
|
||||
LOGI(@"Preflight complete");
|
||||
if (self.syncState.uploadLogURL) {
|
||||
[self logUpload];
|
||||
} else {
|
||||
[self eventUpload];
|
||||
}
|
||||
} else {
|
||||
LOGE(@"Preflight failed, aborting run");
|
||||
exit(1);
|
||||
}
|
||||
}];
|
||||
SNTCommandSyncPreflight *p = [[SNTCommandSyncPreflight alloc] initWithState:self.syncState];
|
||||
if ([p sync]) {
|
||||
LOGD(@"Preflight complete");
|
||||
if (self.syncState.uploadLogURL) {
|
||||
return [self logUpload];
|
||||
} else {
|
||||
return [self eventUpload];
|
||||
}
|
||||
} else {
|
||||
LOGE(@"Preflight failed, aborting run");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)logUpload {
|
||||
[SNTCommandSyncLogUpload performSyncInSession:self.session
|
||||
syncState:self.syncState
|
||||
daemonConn:self.daemonConn
|
||||
completionHandler:^(BOOL success) {
|
||||
if (success) {
|
||||
LOGI(@"Log upload complete");
|
||||
} else {
|
||||
LOGE(@"Log upload failed, continuing anyway");
|
||||
}
|
||||
[self eventUpload];
|
||||
|
||||
}];
|
||||
SNTCommandSyncLogUpload *p = [[SNTCommandSyncLogUpload alloc] initWithState:self.syncState];
|
||||
if ([p sync]) {
|
||||
LOGD(@"Log upload complete");
|
||||
} else {
|
||||
LOGE(@"Log upload failed, continuing anyway");
|
||||
}
|
||||
return [self eventUpload];
|
||||
}
|
||||
|
||||
- (void)eventUpload {
|
||||
[SNTCommandSyncEventUpload performSyncInSession:self.session
|
||||
syncState:self.syncState
|
||||
daemonConn:self.daemonConn
|
||||
completionHandler:^(BOOL success) {
|
||||
if (success) {
|
||||
LOGI(@"Event upload complete");
|
||||
[self ruleDownload];
|
||||
} else {
|
||||
LOGE(@"Event upload failed, aborting run");
|
||||
exit(1);
|
||||
}
|
||||
}];
|
||||
SNTCommandSyncEventUpload *p = [[SNTCommandSyncEventUpload alloc] initWithState:self.syncState];
|
||||
if ([p sync]) {
|
||||
LOGD(@"Event upload complete");
|
||||
return [self ruleDownload];
|
||||
} else {
|
||||
LOGE(@"Event upload failed, aborting run");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)eventUploadSingleEvent:(NSString *)sha256 {
|
||||
[SNTCommandSyncEventUpload uploadSingleEventWithSHA256:sha256
|
||||
session:self.session
|
||||
syncState:self.syncState
|
||||
daemonConn:self.daemonConn
|
||||
completionHandler:^(BOOL success) {
|
||||
if (success) {
|
||||
LOGI(@"Event upload complete");
|
||||
exit(0);
|
||||
} else {
|
||||
LOGW(@"Event upload failed");
|
||||
exit(1);
|
||||
}
|
||||
}];
|
||||
SNTCommandSyncEventUpload *p = [[SNTCommandSyncEventUpload alloc] initWithState:self.syncState];
|
||||
if ([p syncSingleEventWithSHA256:sha256]) {
|
||||
LOGD(@"Event upload complete");
|
||||
exit(0);
|
||||
} else {
|
||||
LOGE(@"Event upload failed");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)ruleDownload {
|
||||
[SNTCommandSyncRuleDownload performSyncInSession:self.session
|
||||
syncState:self.syncState
|
||||
daemonConn:self.daemonConn
|
||||
completionHandler:^(BOOL success) {
|
||||
if (success) {
|
||||
LOGI(@"Rule download complete");
|
||||
[self postflight];
|
||||
} else {
|
||||
LOGE(@"Rule download failed, aborting run");
|
||||
exit(1);
|
||||
}
|
||||
}];
|
||||
SNTCommandSyncRuleDownload *p = [[SNTCommandSyncRuleDownload alloc] initWithState:self.syncState];
|
||||
if ([p sync]) {
|
||||
LOGD(@"Rule download complete");
|
||||
if (self.syncState.bundleBinaryRequests.count) {
|
||||
return [self eventUploadBundleBinaries];
|
||||
}
|
||||
return [self postflight];
|
||||
} else {
|
||||
LOGE(@"Rule download failed, aborting run");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)eventUploadBundleBinaries {
|
||||
SNTCommandSyncEventUpload *p = [[SNTCommandSyncEventUpload alloc] initWithState:self.syncState];
|
||||
if ([p syncBundleEvents]) {
|
||||
LOGD(@"Event Upload bundle binaries complete");
|
||||
} else {
|
||||
LOGW(@"Event Upload bundle binary search failed");
|
||||
}
|
||||
return [self postflight];
|
||||
}
|
||||
|
||||
- (void)postflight {
|
||||
[SNTCommandSyncPostflight performSyncInSession:self.session
|
||||
syncState:self.syncState
|
||||
daemonConn:self.daemonConn
|
||||
completionHandler:^(BOOL success) {
|
||||
if (success) {
|
||||
LOGI(@"Postflight complete");
|
||||
LOGI(@"Sync completed successfully");
|
||||
exit(0);
|
||||
} else {
|
||||
LOGE(@"Postflight failed");
|
||||
exit(1);
|
||||
}
|
||||
}];
|
||||
SNTCommandSyncPostflight *p = [[SNTCommandSyncPostflight alloc] initWithState:self.syncState];
|
||||
if ([p sync]) {
|
||||
LOGD(@"Postflight complete");
|
||||
LOGI(@"Sync completed successfully");
|
||||
exit(0);
|
||||
} else {
|
||||
LOGE(@"Postflight failed");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,10 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
extern NSString *const kURLPreflight;
|
||||
extern NSString *const kURLEventUpload;
|
||||
extern NSString *const kURLRuleDownload;
|
||||
extern NSString *const kURLPostflight;
|
||||
extern NSString *const kXSRFToken;
|
||||
|
||||
extern NSString *const kSerialNumber;
|
||||
extern NSString *const kHostname;
|
||||
@@ -32,6 +29,8 @@ extern NSString *const kClientModeLockdown;
|
||||
extern NSString *const kCleanSync;
|
||||
extern NSString *const kWhitelistRegex;
|
||||
extern NSString *const kBlacklistRegex;
|
||||
extern NSString *const kBinaryRuleCount;
|
||||
extern NSString *const kCertificateRuleCount;
|
||||
|
||||
extern NSString *const kEvents;
|
||||
extern NSString *const kFileSHA256;
|
||||
@@ -49,10 +48,11 @@ extern NSString *const kDecisionBlockBinary;
|
||||
extern NSString *const kDecisionBlockCertificate;
|
||||
extern NSString *const kDecisionBlockScope;
|
||||
extern NSString *const kDecisionUnknown;
|
||||
extern NSString *const kDecisionRelatedBinary;
|
||||
extern NSString *const kDecisionBundleBinary;
|
||||
extern NSString *const kLoggedInUsers;
|
||||
extern NSString *const kCurrentSessions;
|
||||
extern NSString *const kFileBundleID;
|
||||
extern NSString *const kFileBundlePath;
|
||||
extern NSString *const kFileBundleName;
|
||||
extern NSString *const kFileBundleVersion;
|
||||
extern NSString *const kFileBundleShortVersionString;
|
||||
@@ -70,6 +70,7 @@ extern NSString *const kQuarantineDataURL;
|
||||
extern NSString *const kQuarantineRefererURL;
|
||||
extern NSString *const kQuarantineTimestamp;
|
||||
extern NSString *const kQuarantineAgentBundleID;
|
||||
extern NSString *const kEventUploadBundleBinaries;
|
||||
|
||||
extern NSString *const kLogUploadField;
|
||||
|
||||
@@ -14,10 +14,7 @@
|
||||
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
|
||||
NSString *const kURLPreflight = @"preflight/";
|
||||
NSString *const kURLEventUpload = @"eventupload/";
|
||||
NSString *const kURLRuleDownload = @"ruledownload/";
|
||||
NSString *const kURLPostflight = @"postflight/";
|
||||
NSString *const kXSRFToken = @"X-XSRF-TOKEN";
|
||||
|
||||
NSString *const kSerialNumber = @"serial_num";
|
||||
NSString *const kHostname = @"hostname";
|
||||
@@ -34,6 +31,8 @@ NSString *const kClientModeLockdown = @"LOCKDOWN";
|
||||
NSString *const kCleanSync = @"clean_sync";
|
||||
NSString *const kWhitelistRegex = @"whitelist_regex";
|
||||
NSString *const kBlacklistRegex = @"blacklist_regex";
|
||||
NSString *const kBinaryRuleCount = @"binary_rule_count";
|
||||
NSString *const kCertificateRuleCount = @"certificate_rule_count";
|
||||
|
||||
NSString *const kEvents = @"events";
|
||||
NSString *const kFileSHA256 = @"file_sha256";
|
||||
@@ -51,10 +50,11 @@ NSString *const kDecisionBlockBinary = @"BLOCK_BINARY";
|
||||
NSString *const kDecisionBlockCertificate = @"BLOCK_CERTIFICATE";
|
||||
NSString *const kDecisionBlockScope = @"BLOCK_SCOPE";
|
||||
NSString *const kDecisionUnknown = @"UNKNOWN";
|
||||
NSString *const kDecisionRelatedBinary = @"RELATED_BINARY";
|
||||
NSString *const kDecisionBundleBinary = @"BUNDLE_BINARY";
|
||||
NSString *const kLoggedInUsers = @"logged_in_users";
|
||||
NSString *const kCurrentSessions = @"current_sessions";
|
||||
NSString *const kFileBundleID = @"file_bundle_id";
|
||||
NSString *const kFileBundlePath = @"file_bundle_path";
|
||||
NSString *const kFileBundleName = @"file_bundle_name";
|
||||
NSString *const kFileBundleVersion = @"file_bundle_version";
|
||||
NSString *const kFileBundleShortVersionString = @"file_bundle_version_string";
|
||||
@@ -72,6 +72,7 @@ NSString *const kQuarantineDataURL = @"quarantine_data_url";
|
||||
NSString *const kQuarantineRefererURL = @"quarantine_referer_url";
|
||||
NSString *const kQuarantineTimestamp = @"quarantine_timestamp";
|
||||
NSString *const kQuarantineAgentBundleID = @"quarantine_agent_bundle_id";
|
||||
NSString *const kEventUploadBundleBinaries = @"event_upload_bundle_binaries";
|
||||
|
||||
NSString *const kLogUploadField = @"files";
|
||||
|
||||
@@ -12,14 +12,12 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
@class SNTCommandSyncState;
|
||||
@class SNTXPCConnection;
|
||||
#import "SNTCommandSyncStage.h"
|
||||
|
||||
@interface SNTCommandSyncPreflight : NSObject
|
||||
@interface SNTCommandSyncEventUpload : SNTCommandSyncStage
|
||||
|
||||
+ (void)performSyncInSession:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler;
|
||||
- (BOOL)syncSingleEventWithSHA256:(NSString *)sha256;
|
||||
|
||||
- (BOOL)syncBundleEvents;
|
||||
|
||||
@end
|
||||
222
Source/santactl/Commands/sync/SNTCommandSyncEventUpload.m
Normal file
222
Source/santactl/Commands/sync/SNTCommandSyncEventUpload.m
Normal file
@@ -0,0 +1,222 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommandSyncEventUpload.h"
|
||||
|
||||
#include "SNTLogging.h"
|
||||
|
||||
#import "MOLCertificate.h"
|
||||
#import "MOLCodesignChecker.h"
|
||||
#import "NSData+Zlib.h"
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
#import "SNTCommandSyncState.h"
|
||||
#import "SNTFileInfo.h"
|
||||
#import "SNTStoredEvent.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@implementation SNTCommandSyncEventUpload
|
||||
|
||||
- (NSURL *)stageURL {
|
||||
NSString *stageName = [@"eventupload" stringByAppendingFormat:@"/%@", self.syncState.machineID];
|
||||
return [NSURL URLWithString:stageName relativeToURL:self.syncState.syncBaseURL];
|
||||
}
|
||||
|
||||
- (BOOL)sync {
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
[[self.daemonConn remoteObjectProxy] databaseEventsPending:^(NSArray *events) {
|
||||
if (events.count) {
|
||||
[self uploadEvents:events];
|
||||
}
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
return (dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER) == 0);
|
||||
}
|
||||
|
||||
- (BOOL)syncSingleEventWithSHA256:(NSString *)sha256 {
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
[[self.daemonConn remoteObjectProxy] databaseEventForSHA256:sha256 reply:^(SNTStoredEvent *e) {
|
||||
if (e) {
|
||||
[self uploadEvents:@[ e ]];
|
||||
}
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
return (dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER) == 0);
|
||||
}
|
||||
|
||||
- (BOOL)syncBundleEvents {
|
||||
NSMutableArray *newEvents = [NSMutableArray array];
|
||||
for (NSString *bundlePath in self.syncState.bundleBinaryRequests) {
|
||||
[newEvents addObjectsFromArray:[self findRelatedBinaries:bundlePath]];
|
||||
}
|
||||
return [self uploadEvents:newEvents];
|
||||
}
|
||||
|
||||
- (BOOL)uploadEvents:(NSArray *)events {
|
||||
NSMutableArray *uploadEvents = [[NSMutableArray alloc] init];
|
||||
|
||||
NSMutableDictionary *eventIds = [NSMutableDictionary dictionaryWithCapacity:events.count];
|
||||
for (SNTStoredEvent *event in events) {
|
||||
[uploadEvents addObject:[self dictionaryForEvent:event]];
|
||||
eventIds[event.idx] = @YES;
|
||||
if (uploadEvents.count >= self.syncState.eventBatchSize) break;
|
||||
}
|
||||
|
||||
NSDictionary *r = [self performRequest:[self requestWithDictionary:@{ kEvents: uploadEvents }]];
|
||||
if (!r) return NO;
|
||||
|
||||
// Keep track of bundle search requests
|
||||
self.syncState.bundleBinaryRequests = r[kEventUploadBundleBinaries];
|
||||
|
||||
LOGI(@"Uploaded %lu events", uploadEvents.count);
|
||||
|
||||
// Remove event IDs. For Bundle Events the ID is 0 so nothing happens.
|
||||
[[self.daemonConn remoteObjectProxy] databaseRemoveEventsWithIDs:[eventIds allKeys]];
|
||||
|
||||
// See if there are any events remaining to upload
|
||||
if (uploadEvents.count < events.count) {
|
||||
NSRange nextEventsRange = NSMakeRange(uploadEvents.count, events.count - uploadEvents.count);
|
||||
NSArray *nextEvents = [events subarrayWithRange:nextEventsRange];
|
||||
return [self uploadEvents:nextEvents];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSDictionary *)dictionaryForEvent:(SNTStoredEvent *)event {
|
||||
#define ADDKEY(dict, key, value) if (value) dict[key] = value
|
||||
NSMutableDictionary *newEvent = [NSMutableDictionary dictionary];
|
||||
|
||||
ADDKEY(newEvent, kFileSHA256, event.fileSHA256);
|
||||
ADDKEY(newEvent, kFilePath, [event.filePath stringByDeletingLastPathComponent]);
|
||||
ADDKEY(newEvent, kFileName, [event.filePath lastPathComponent]);
|
||||
ADDKEY(newEvent, kExecutingUser, event.executingUser);
|
||||
ADDKEY(newEvent, kExecutionTime, @([event.occurrenceDate timeIntervalSince1970]));
|
||||
ADDKEY(newEvent, kLoggedInUsers, event.loggedInUsers);
|
||||
ADDKEY(newEvent, kCurrentSessions, event.currentSessions);
|
||||
|
||||
switch (event.decision) {
|
||||
case SNTEventStateAllowUnknown: ADDKEY(newEvent, kDecision, kDecisionAllowUnknown); break;
|
||||
case SNTEventStateAllowBinary: ADDKEY(newEvent, kDecision, kDecisionAllowBinary); break;
|
||||
case SNTEventStateAllowCertificate:
|
||||
ADDKEY(newEvent, kDecision, kDecisionAllowCertificate);
|
||||
break;
|
||||
case SNTEventStateAllowScope: ADDKEY(newEvent, kDecision, kDecisionAllowScope); break;
|
||||
case SNTEventStateBlockUnknown: ADDKEY(newEvent, kDecision, kDecisionBlockUnknown); break;
|
||||
case SNTEventStateBlockBinary: ADDKEY(newEvent, kDecision, kDecisionBlockBinary); break;
|
||||
case SNTEventStateBlockCertificate:
|
||||
ADDKEY(newEvent, kDecision, kDecisionBlockCertificate);
|
||||
break;
|
||||
case SNTEventStateBlockScope: ADDKEY(newEvent, kDecision, kDecisionBlockScope); break;
|
||||
case SNTEventStateBundleBinary: ADDKEY(newEvent, kDecision, kDecisionBundleBinary); break;
|
||||
default: ADDKEY(newEvent, kDecision, kDecisionUnknown);
|
||||
}
|
||||
|
||||
ADDKEY(newEvent, kFileBundleID, event.fileBundleID);
|
||||
ADDKEY(newEvent, kFileBundlePath, event.fileBundlePath);
|
||||
ADDKEY(newEvent, kFileBundleName, event.fileBundleName);
|
||||
ADDKEY(newEvent, kFileBundleVersion, event.fileBundleVersion);
|
||||
ADDKEY(newEvent, kFileBundleShortVersionString, event.fileBundleVersionString);
|
||||
|
||||
ADDKEY(newEvent, kPID, event.pid);
|
||||
ADDKEY(newEvent, kPPID, event.ppid);
|
||||
ADDKEY(newEvent, kParentName, event.parentName);
|
||||
|
||||
ADDKEY(newEvent, kQuarantineDataURL, event.quarantineDataURL);
|
||||
ADDKEY(newEvent, kQuarantineRefererURL, event.quarantineRefererURL);
|
||||
ADDKEY(newEvent, kQuarantineTimestamp, @([event.quarantineTimestamp timeIntervalSince1970]));
|
||||
ADDKEY(newEvent, kQuarantineAgentBundleID, event.quarantineAgentBundleID);
|
||||
|
||||
NSMutableArray *signingChain = [NSMutableArray arrayWithCapacity:event.signingChain.count];
|
||||
for (NSUInteger i = 0; i < event.signingChain.count; ++i) {
|
||||
MOLCertificate *cert = [event.signingChain objectAtIndex:i];
|
||||
|
||||
NSMutableDictionary *certDict = [NSMutableDictionary dictionary];
|
||||
ADDKEY(certDict, kCertSHA256, cert.SHA256);
|
||||
ADDKEY(certDict, kCertCN, cert.commonName);
|
||||
ADDKEY(certDict, kCertOrg, cert.orgName);
|
||||
ADDKEY(certDict, kCertOU, cert.orgUnit);
|
||||
ADDKEY(certDict, kCertValidFrom, @([cert.validFrom timeIntervalSince1970]));
|
||||
ADDKEY(certDict, kCertValidUntil, @([cert.validUntil timeIntervalSince1970]));
|
||||
|
||||
[signingChain addObject:certDict];
|
||||
}
|
||||
newEvent[kSigningChain] = signingChain;
|
||||
|
||||
return newEvent;
|
||||
#undef ADDKEY
|
||||
}
|
||||
|
||||
// Find binaries within a bundle given the bundle's path
|
||||
// Searches for 10 minutes, creating new events.
|
||||
- (NSArray *)findRelatedBinaries:(NSString *)path {
|
||||
SNTFileInfo *requestedPath = [[SNTFileInfo alloc] initWithPath:path];
|
||||
|
||||
// Prevent processing the same bundle twice.
|
||||
static NSMutableDictionary *previouslyProcessedBundles;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
previouslyProcessedBundles = [NSMutableDictionary dictionary];
|
||||
});
|
||||
if (previouslyProcessedBundles[requestedPath.bundleIdentifier]) return nil;
|
||||
previouslyProcessedBundles[requestedPath.bundleIdentifier] = @YES;
|
||||
|
||||
NSMutableArray *relatedEvents = [NSMutableArray array];
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
__block BOOL shouldCancel = NO;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
||||
NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:path];
|
||||
NSString *file;
|
||||
|
||||
while (file = [dirEnum nextObject]) {
|
||||
@autoreleasepool {
|
||||
if (shouldCancel) break;
|
||||
if ([dirEnum fileAttributes][NSFileType] != NSFileTypeRegular) continue;
|
||||
|
||||
file = [path stringByAppendingPathComponent:file];
|
||||
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:file];
|
||||
if (fi.isExecutable) {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
se.filePath = fi.path;
|
||||
se.fileSHA256 = fi.SHA256;
|
||||
se.decision = SNTEventStateBundleBinary;
|
||||
se.fileBundleID = fi.bundleIdentifier;
|
||||
se.fileBundleName = fi.bundleName;
|
||||
se.fileBundlePath = fi.bundlePath;
|
||||
se.fileBundleVersion = fi.bundleVersion;
|
||||
se.fileBundleVersionString = fi.bundleShortVersionString;
|
||||
|
||||
MOLCodesignChecker *cs = [[MOLCodesignChecker alloc] initWithBinaryPath:se.filePath];
|
||||
se.signingChain = cs.certificates;
|
||||
|
||||
[relatedEvents addObject:[self dictionaryForEvent:se]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_semaphore_signal(sema);
|
||||
});
|
||||
|
||||
// Give the search up to 10m per bundle to run.
|
||||
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 600))) {
|
||||
shouldCancel = YES;
|
||||
LOGD(@"Timed out while searching for related events at path %@", path);
|
||||
}
|
||||
|
||||
return relatedEvents;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,6 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "SantaPIDAndPPID.h"
|
||||
#import "SNTCommandSyncStage.h"
|
||||
|
||||
OSDefineMetaClassAndStructors(SantaPIDAndPPID, OSObject);
|
||||
@interface SNTCommandSyncLogUpload : SNTCommandSyncStage
|
||||
@end
|
||||
@@ -15,49 +15,40 @@
|
||||
#import "SNTCommandSyncLogUpload.h"
|
||||
|
||||
#import "NSData+Zlib.h"
|
||||
|
||||
#include "SNTCommonEnums.h"
|
||||
#include "SNTLogging.h"
|
||||
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
#import "SNTCommandSyncState.h"
|
||||
#import "SNTCommonEnums.h"
|
||||
#import "SNTLogging.h"
|
||||
|
||||
@implementation SNTCommandSyncLogUpload
|
||||
|
||||
+ (void)performSyncInSession:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler {
|
||||
NSURL *url = syncState.uploadLogURL;
|
||||
NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:url];
|
||||
[req setHTTPMethod:@"POST"];
|
||||
NSString *boundary = @"----santa-sync-upload-boundary";
|
||||
- (NSString *)stageName {
|
||||
return @"logupload";
|
||||
}
|
||||
|
||||
- (NSURL *)stageURL {
|
||||
return self.syncState.uploadLogURL;
|
||||
}
|
||||
|
||||
- (BOOL)sync {
|
||||
NSMutableURLRequest *req = [self requestWithDictionary:nil];
|
||||
|
||||
NSString *boundary = @"----santa-sync-upload-boundary";
|
||||
NSString *contentType =
|
||||
[NSString stringWithFormat:@"multipart/form-data; charset=UTF-8; boundary=%@", boundary];
|
||||
[req setValue:contentType forHTTPHeaderField:@"Content-Type"];
|
||||
|
||||
NSArray *logsToUpload = [self logsToUpload];
|
||||
[req setHTTPBody:[self requestBodyWithLogs:logsToUpload andBoundary:boundary]];
|
||||
|
||||
// Upload the logs
|
||||
[[session uploadTaskWithRequest:req
|
||||
fromData:[self requestBodyWithLogs:logsToUpload andBoundary:boundary]
|
||||
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
long statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if (statusCode != 200) {
|
||||
LOGE(@"HTTP Response: %ld %@",
|
||||
statusCode,
|
||||
[[NSHTTPURLResponse localizedStringForStatusCode:statusCode] capitalizedString]);
|
||||
LOGD(@"%@", error);
|
||||
handler(NO);
|
||||
} else {
|
||||
LOGI(@"Uploaded %lu logs", [logsToUpload count]);
|
||||
handler(YES);
|
||||
}
|
||||
}] resume];
|
||||
NSDictionary *d = [self performRequest:req];
|
||||
if (!d) return NO;
|
||||
|
||||
LOGI(@"Uploaded %lu logs", logsToUpload.count);
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (NSData *)requestBodyWithLogs:(NSArray *)logsToUpload andBoundary:(NSString *)boundary {
|
||||
- (NSData *)requestBodyWithLogs:(NSArray *)logsToUpload andBoundary:(NSString *)boundary {
|
||||
// Prepare the body of the request, encoded as a multipart/form-data.
|
||||
// Along the way, gzip the individual log files and append .gz to their filenames.
|
||||
NSMutableData *reqBody = [[NSMutableData alloc] init];
|
||||
@@ -80,7 +71,7 @@
|
||||
return reqBody;
|
||||
}
|
||||
|
||||
+ (NSArray *)logsToUpload {
|
||||
- (NSArray *)logsToUpload {
|
||||
// General logs
|
||||
NSMutableArray *logsToUpload = [@[ @"/var/log/santa.log",
|
||||
@"/var/log/system.log" ] mutableCopy];
|
||||
@@ -12,14 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
@class SNTCommandSyncState;
|
||||
@class SNTXPCConnection;
|
||||
|
||||
@interface SNTCommandSyncLogUpload : NSObject
|
||||
|
||||
+ (void)performSyncInSession:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler;
|
||||
#import "SNTCommandSyncStage.h"
|
||||
|
||||
@interface SNTCommandSyncPostflight : SNTCommandSyncStage
|
||||
@end
|
||||
82
Source/santactl/Commands/sync/SNTCommandSyncPostflight.m
Normal file
82
Source/santactl/Commands/sync/SNTCommandSyncPostflight.m
Normal file
@@ -0,0 +1,82 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommandSyncPostflight.h"
|
||||
|
||||
#include "SNTLogging.h"
|
||||
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
#import "SNTCommandSyncState.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@implementation SNTCommandSyncPostflight
|
||||
|
||||
- (NSURL *)stageURL {
|
||||
NSString *stageName = [@"postflight" stringByAppendingFormat:@"/%@", self.syncState.machineID];
|
||||
return [NSURL URLWithString:stageName relativeToURL:self.syncState.syncBaseURL];
|
||||
}
|
||||
|
||||
- (BOOL)sync {
|
||||
NSDictionary *r = [self performRequest:[self requestWithDictionary:nil]];
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
void (^replyBlock)() = ^{
|
||||
dispatch_group_leave(group);
|
||||
};
|
||||
|
||||
// Set client mode if it changed
|
||||
if (self.syncState.clientMode) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setClientMode:self.syncState.clientMode
|
||||
reply:replyBlock];
|
||||
}
|
||||
|
||||
// Update backoff interval
|
||||
NSString *backoffInterval = r[kBackoffInterval];
|
||||
if (backoffInterval) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setNextSyncInterval:[backoffInterval intValue]
|
||||
reply:replyBlock];
|
||||
}
|
||||
|
||||
// Remove clean sync flag if we did a clean sync
|
||||
if (self.syncState.cleanSync) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setSyncCleanRequired:NO reply:replyBlock];
|
||||
}
|
||||
|
||||
// Update whitelist/blacklist regexes
|
||||
if (self.syncState.whitelistRegex) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setWhitelistPathRegex:self.syncState.whitelistRegex
|
||||
reply:replyBlock];
|
||||
}
|
||||
if (self.syncState.blacklistRegex) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setBlacklistPathRegex:self.syncState.blacklistRegex
|
||||
reply:replyBlock];
|
||||
}
|
||||
|
||||
// Update last sync success
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setSyncLastSuccess:[NSDate date] reply:replyBlock];
|
||||
|
||||
// Wait for dispatch group
|
||||
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,14 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
@class SNTCommandSyncState;
|
||||
@class SNTXPCConnection;
|
||||
|
||||
@interface SNTCommandSyncPostflight : NSObject
|
||||
|
||||
+ (void)performSyncInSession:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler;
|
||||
#import "SNTCommandSyncStage.h"
|
||||
|
||||
@interface SNTCommandSyncPreflight : SNTCommandSyncStage
|
||||
@end
|
||||
90
Source/santactl/Commands/sync/SNTCommandSyncPreflight.m
Normal file
90
Source/santactl/Commands/sync/SNTCommandSyncPreflight.m
Normal file
@@ -0,0 +1,90 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommandSyncPreflight.h"
|
||||
|
||||
#include "SNTKernelCommon.h"
|
||||
#include "SNTLogging.h"
|
||||
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
#import "SNTCommandSyncState.h"
|
||||
#import "SNTConfigurator.h"
|
||||
#import "SNTSystemInfo.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@implementation SNTCommandSyncPreflight
|
||||
|
||||
- (NSURL *)stageURL {
|
||||
NSString *stageName = [@"preflight" stringByAppendingFormat:@"/%@", self.syncState.machineID];
|
||||
return [NSURL URLWithString:stageName relativeToURL:self.syncState.syncBaseURL];
|
||||
}
|
||||
|
||||
- (BOOL)sync {
|
||||
NSMutableDictionary *requestDict = [NSMutableDictionary dictionary];
|
||||
requestDict[kSerialNumber] = [SNTSystemInfo serialNumber];
|
||||
requestDict[kHostname] = [SNTSystemInfo longHostname];
|
||||
requestDict[kOSVer] = [SNTSystemInfo osVersion];
|
||||
requestDict[kOSBuild] = [SNTSystemInfo osBuild];
|
||||
requestDict[kSantaVer] = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
||||
requestDict[kPrimaryUser] = self.syncState.machineOwner;
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
[[self.daemonConn remoteObjectProxy] databaseRuleCounts:^(int64_t binary, int64_t certificate) {
|
||||
requestDict[kBinaryRuleCount] = @(binary);
|
||||
requestDict[kCertificateRuleCount] = @(certificate);
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC));
|
||||
|
||||
// If user requested it or we've never had a successful sync, try from a clean slate.
|
||||
if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--clean"] ||
|
||||
[[SNTConfigurator configurator] syncCleanRequired]) {
|
||||
requestDict[kRequestCleanSync] = @YES;
|
||||
}
|
||||
|
||||
NSURLRequest *req = [self requestWithDictionary:requestDict];
|
||||
NSDictionary *resp = [self performRequest:req];
|
||||
|
||||
if (!resp) return NO;
|
||||
|
||||
self.syncState.eventBatchSize = [resp[kBatchSize] intValue];
|
||||
if (self.syncState.eventBatchSize == 0) {
|
||||
self.syncState.eventBatchSize = 50;
|
||||
}
|
||||
|
||||
self.syncState.uploadLogURL = [NSURL URLWithString:resp[kUploadLogsURL]];
|
||||
|
||||
if ([resp[kClientMode] isEqual:kClientModeMonitor]) {
|
||||
self.syncState.clientMode = SNTClientModeMonitor;
|
||||
} else if ([resp[kClientMode] isEqual:kClientModeLockdown]) {
|
||||
self.syncState.clientMode = SNTClientModeLockdown;
|
||||
}
|
||||
|
||||
if ([resp[kWhitelistRegex] isKindOfClass:[NSString class]]) {
|
||||
self.syncState.whitelistRegex = resp[kWhitelistRegex];
|
||||
}
|
||||
|
||||
if ([resp[kBlacklistRegex] isKindOfClass:[NSString class]]) {
|
||||
self.syncState.blacklistRegex = resp[kBlacklistRegex];
|
||||
}
|
||||
|
||||
if ([resp[kCleanSync] boolValue]) {
|
||||
self.syncState.cleanSync = YES;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
18
Source/santactl/Commands/sync/SNTCommandSyncRuleDownload.h
Normal file
18
Source/santactl/Commands/sync/SNTCommandSyncRuleDownload.h
Normal file
@@ -0,0 +1,18 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommandSyncStage.h"
|
||||
|
||||
@interface SNTCommandSyncRuleDownload : SNTCommandSyncStage
|
||||
@end
|
||||
111
Source/santactl/Commands/sync/SNTCommandSyncRuleDownload.m
Normal file
111
Source/santactl/Commands/sync/SNTCommandSyncRuleDownload.m
Normal file
@@ -0,0 +1,111 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommandSyncRuleDownload.h"
|
||||
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
#import "SNTCommandSyncState.h"
|
||||
#import "SNTRule.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
#include "SNTLogging.h"
|
||||
|
||||
@implementation SNTCommandSyncRuleDownload
|
||||
|
||||
- (NSURL *)stageURL {
|
||||
NSString *stageName = [@"ruledownload" stringByAppendingFormat:@"/%@", self.syncState.machineID];
|
||||
return [NSURL URLWithString:stageName relativeToURL:self.syncState.syncBaseURL];
|
||||
}
|
||||
|
||||
- (BOOL)sync {
|
||||
self.syncState.downloadedRules = [NSMutableArray array];
|
||||
return [self ruleDownloadWithCursor:nil];
|
||||
}
|
||||
|
||||
- (BOOL)ruleDownloadWithCursor:(NSString *)cursor {
|
||||
NSDictionary *requestDict = (cursor ? @{kCursor : cursor} : @{});
|
||||
|
||||
NSDictionary *resp = [self performRequest:[self requestWithDictionary:requestDict]];
|
||||
if (!resp) return NO;
|
||||
|
||||
for (NSDictionary *rule in resp[kRules]) {
|
||||
SNTRule *r = [self ruleFromDictionary:rule];
|
||||
if (r) [self.syncState.downloadedRules addObject:r];
|
||||
}
|
||||
|
||||
if (resp[kCursor]) {
|
||||
return [self ruleDownloadWithCursor:resp[kCursor]];
|
||||
}
|
||||
|
||||
if (!self.syncState.downloadedRules.count) return YES;
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
__block NSError *error;
|
||||
[[self.daemonConn remoteObjectProxy] databaseRuleAddRules:self.syncState.downloadedRules
|
||||
cleanSlate:self.syncState.cleanSync
|
||||
reply:^(NSError *e) {
|
||||
error = e;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 300 * NSEC_PER_SEC));
|
||||
|
||||
if (error) {
|
||||
LOGE(@"Failed to add rule(s) to database: %@", error.localizedDescription);
|
||||
LOGD(@"Failure reason: %@", error.localizedFailureReason);
|
||||
return NO;
|
||||
}
|
||||
|
||||
LOGI(@"Added %lu rules", self.syncState.downloadedRules.count);
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleFromDictionary:(NSDictionary *)dict {
|
||||
if (![dict isKindOfClass:[NSDictionary class]]) return nil;
|
||||
|
||||
SNTRule *newRule = [[SNTRule alloc] init];
|
||||
newRule.shasum = dict[kRuleSHA256];
|
||||
if (newRule.shasum.length != 64) return nil;
|
||||
|
||||
NSString *policyString = dict[kRulePolicy];
|
||||
if ([policyString isEqual:kRulePolicyWhitelist]) {
|
||||
newRule.state = SNTRuleStateWhitelist;
|
||||
} else if ([policyString isEqual:kRulePolicyBlacklist]) {
|
||||
newRule.state = SNTRuleStateBlacklist;
|
||||
} else if ([policyString isEqual:kRulePolicySilentBlacklist]) {
|
||||
newRule.state = SNTRuleStateSilentBlacklist;
|
||||
} else if ([policyString isEqual:kRulePolicyRemove]) {
|
||||
newRule.state = SNTRuleStateRemove;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *ruleTypeString = dict[kRuleType];
|
||||
if ([ruleTypeString isEqual:kRuleTypeBinary]) {
|
||||
newRule.type = SNTRuleTypeBinary;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeCertificate]) {
|
||||
newRule.type = SNTRuleTypeCertificate;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *customMsg = dict[kRuleCustomMsg];
|
||||
if (customMsg.length) {
|
||||
newRule.customMsg = customMsg;
|
||||
}
|
||||
|
||||
return newRule;
|
||||
}
|
||||
|
||||
@end
|
||||
72
Source/santactl/Commands/sync/SNTCommandSyncStage.h
Normal file
72
Source/santactl/Commands/sync/SNTCommandSyncStage.h
Normal file
@@ -0,0 +1,72 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
@class SNTCommandSyncState;
|
||||
@class SNTXPCConnection;
|
||||
|
||||
@interface SNTCommandSyncStage : NSObject
|
||||
|
||||
@property(readonly, nonnull) NSURLSession *urlSession;
|
||||
@property(readonly, nonnull) SNTCommandSyncState *syncState;
|
||||
@property(readonly, nonnull) SNTXPCConnection *daemonConn;
|
||||
|
||||
/**
|
||||
Initialize this stage. Designated initializer.
|
||||
|
||||
@param syncState A holder for state used across requests
|
||||
*/
|
||||
- (nullable instancetype)initWithState:(nonnull SNTCommandSyncState *)state NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (nullable instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/**
|
||||
Performs this sync stage.
|
||||
|
||||
@return YES if sync was successful.
|
||||
*/
|
||||
- (BOOL)sync;
|
||||
|
||||
/**
|
||||
The URL for this stage.
|
||||
|
||||
@return The NSURL for this stage.
|
||||
*/
|
||||
- (nonnull NSURL *)stageURL;
|
||||
|
||||
#pragma mark Internal Helpers
|
||||
|
||||
/**
|
||||
Creates an NSMutableURLRequest pointing at the URL for this stage and containing the JSON-encoded
|
||||
data passed in as a dictionary.
|
||||
|
||||
@param dictionary The values to POST to the server.
|
||||
*/
|
||||
- (nullable NSMutableURLRequest *)requestWithDictionary:(nullable NSDictionary *)dictionary;
|
||||
|
||||
/**
|
||||
Perform the passed in request and attempt to parse the response as JSON into a dictionary.
|
||||
|
||||
@param request The request to perform
|
||||
@param timeout The number of seconds to allow the request to run before timing out.
|
||||
|
||||
@return A populated dictionary if the response data was JSON, an empty dictionary if not and nil
|
||||
if the request failed for any reason.
|
||||
*/
|
||||
- (nullable NSDictionary *)performRequest:(nonnull NSURLRequest *)request
|
||||
timeout:(NSTimeInterval)timeout;
|
||||
|
||||
/** Convenience version of performRequest:timeout: using a 30s timeout. */
|
||||
- (nullable NSDictionary *)performRequest:(nonnull NSURLRequest *)request;
|
||||
|
||||
@end
|
||||
195
Source/santactl/Commands/sync/SNTCommandSyncStage.m
Normal file
195
Source/santactl/Commands/sync/SNTCommandSyncStage.m
Normal file
@@ -0,0 +1,195 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommandSyncStage.h"
|
||||
|
||||
#import "NSData+Zlib.h"
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
#import "SNTCommandSyncState.h"
|
||||
#import "SNTLogging.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
|
||||
@interface SNTCommandSyncStage ()
|
||||
|
||||
@property(readwrite) NSURLSession *urlSession;
|
||||
@property(readwrite) SNTCommandSyncState *syncState;
|
||||
@property(readwrite) SNTXPCConnection *daemonConn;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SNTCommandSyncStage
|
||||
|
||||
- (nullable instancetype)initWithState:(nonnull SNTCommandSyncState *)syncState {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_syncState = syncState;
|
||||
_urlSession = syncState.session;
|
||||
_daemonConn = syncState.daemonConn;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)sync {
|
||||
[self doesNotRecognizeSelector:_cmd]; __builtin_unreachable();
|
||||
}
|
||||
|
||||
- (NSString *)stageURL {
|
||||
[self doesNotRecognizeSelector:_cmd]; __builtin_unreachable();
|
||||
}
|
||||
|
||||
- (NSMutableURLRequest *)requestWithDictionary:(NSDictionary *)dictionary {
|
||||
NSData *requestBody = [NSData data];
|
||||
if (dictionary) {
|
||||
NSError *error;
|
||||
requestBody = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&error];
|
||||
if (error) {
|
||||
LOGD(@"Failed to encode JSON request: %@", error);
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:[self stageURL]];
|
||||
[req setHTTPMethod:@"POST"];
|
||||
[req setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||
[req setValue:self.syncState.xsrfToken forHTTPHeaderField:kXSRFToken];
|
||||
|
||||
NSData *compressed = [requestBody zlibCompressed];
|
||||
if (compressed) {
|
||||
requestBody = compressed;
|
||||
[req setValue:@"zlib" forHTTPHeaderField:@"Content-Encoding"];
|
||||
}
|
||||
|
||||
[req setHTTPBody:requestBody];
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
- (NSDictionary *)performRequest:(NSURLRequest *)request timeout:(NSTimeInterval)timeout {
|
||||
NSHTTPURLResponse *response;
|
||||
NSError *error;
|
||||
NSData *data = [self performRequest:request timeout:timeout response:&response error:&error];
|
||||
|
||||
// If the original request failed, attempt to get a new XSRF token and try again.
|
||||
// Unfortunately some servers cause NSURLSession to return 'client cert required' or
|
||||
// 'could not parse response' when a 403 occurs and SSL cert auth is enabled.
|
||||
if ((response.statusCode == 403 ||
|
||||
error.code == NSURLErrorClientCertificateRequired ||
|
||||
error.code == NSURLErrorCannotParseResponse) &&
|
||||
[self fetchXSRFToken]) {
|
||||
NSMutableURLRequest *mutableRequest = [request mutableCopy];
|
||||
[mutableRequest setValue:self.syncState.xsrfToken forHTTPHeaderField:kXSRFToken];
|
||||
return [self performRequest:mutableRequest timeout:timeout];
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
long code;
|
||||
NSString *errStr;
|
||||
if (response.statusCode > 0) {
|
||||
code = response.statusCode;
|
||||
errStr = [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode];
|
||||
} else {
|
||||
code = (long)error.code;
|
||||
errStr = error.localizedDescription;
|
||||
}
|
||||
LOGE(@"HTTP Response: %ld %@", code, errStr);
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (data.length == 0) return @{};
|
||||
|
||||
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:[self stripXssi:data]
|
||||
options:0
|
||||
error:&error];
|
||||
if (error) LOGD(@"Failed to decode JSON response: %@", error);
|
||||
|
||||
return dict ?: @{};
|
||||
}
|
||||
|
||||
- (NSDictionary *)performRequest:(NSURLRequest *)request {
|
||||
return [self performRequest:request timeout:30];
|
||||
}
|
||||
|
||||
#pragma mark Internal Helpers
|
||||
|
||||
/**
|
||||
Perform a data request and capture the returned data, response and error objects.
|
||||
|
||||
@param request, The request to perform
|
||||
@param timeout, The number of seconds to wait before cancelling the request
|
||||
@param response, Return the response details
|
||||
@param error, Return the error details
|
||||
@returns data, The HTTP body of the response
|
||||
*/
|
||||
- (NSData *)performRequest:(NSURLRequest *)request
|
||||
timeout:(NSTimeInterval)timeout
|
||||
response:(out NSHTTPURLResponse **)response
|
||||
error:(out NSError **)error {
|
||||
__block NSData *_data;
|
||||
__block NSHTTPURLResponse *_response;
|
||||
__block NSError *_error;
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
NSURLSessionDataTask *task = [self.urlSession dataTaskWithRequest:request
|
||||
completionHandler:^(NSData *data,
|
||||
NSURLResponse *response,
|
||||
NSError *error) {
|
||||
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
_response = (NSHTTPURLResponse *)response;
|
||||
}
|
||||
_data = data;
|
||||
_error = error;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
[task resume];
|
||||
|
||||
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * timeout))) {
|
||||
[task cancel];
|
||||
}
|
||||
|
||||
if (response) *response = _response;
|
||||
if (error) *error = _error;
|
||||
return _data;
|
||||
}
|
||||
|
||||
- (NSData *)stripXssi:(NSData *)data {
|
||||
static const char xssi[3] = { ']', ')', '}' };
|
||||
if (data.length < 3 || strncmp(data.bytes, xssi, 3)) return data;
|
||||
return [data subdataWithRange:NSMakeRange(3, data.length - 3)];
|
||||
}
|
||||
|
||||
- (BOOL)fetchXSRFToken {
|
||||
__block BOOL success = NO;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{ // only fetch token once per session
|
||||
NSString *stageName = [@"xsrf" stringByAppendingFormat:@"/%@", self.syncState.machineID];
|
||||
NSURL *u = [NSURL URLWithString:stageName relativeToURL:self.syncState.syncBaseURL];
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:u];
|
||||
[request setHTTPMethod:@"POST"];
|
||||
NSHTTPURLResponse *response;
|
||||
[self performRequest:request timeout:10 response:&response error:NULL];
|
||||
if (response.statusCode == 200) {
|
||||
NSDictionary *headers = [response allHeaderFields];
|
||||
[[self.daemonConn remoteObjectProxy] setXsrfToken:headers[kXSRFToken] reply:^{}];
|
||||
self.syncState.xsrfToken = headers[kXSRFToken];
|
||||
LOGD(@"Retrieved new XSRF token");
|
||||
success = YES;
|
||||
} else {
|
||||
LOGD(@"Failed to retrieve XSRF token");
|
||||
}
|
||||
});
|
||||
return success;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,33 +12,48 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "SNTCommonEnums.h"
|
||||
#import "SNTCommonEnums.h"
|
||||
|
||||
@class SNTXPCConnection;
|
||||
|
||||
/// An instance of this class is passed to each stage of the sync process for storing data
|
||||
/// that might be needed in later stages.
|
||||
@interface SNTCommandSyncState : NSObject
|
||||
|
||||
/// The base API URL
|
||||
/// Configured session to use for requests.
|
||||
@property NSURLSession *session;
|
||||
|
||||
/// Connection to the daemon control interface.
|
||||
@property SNTXPCConnection *daemonConn;
|
||||
|
||||
/// The base API URL.
|
||||
@property NSURL *syncBaseURL;
|
||||
|
||||
/// Machine identifier and owner
|
||||
/// An XSRF token to send in the headers with each request.
|
||||
@property NSString *xsrfToken;
|
||||
|
||||
/// Machine identifier and owner.
|
||||
@property(copy) NSString *machineID;
|
||||
@property(copy) NSString *machineOwner;
|
||||
|
||||
/// Clean sync flag, sent from server. If True, all existing rules
|
||||
/// should be deleted before inserting any new rules.
|
||||
/// Settings sent from server during preflight that are set during postflight.
|
||||
@property SNTClientMode clientMode;
|
||||
@property NSString *whitelistRegex;
|
||||
@property NSString *blacklistRegex;
|
||||
|
||||
/// Clean sync flag, if True, all existing rules should be deleted before inserting any new rules.
|
||||
@property BOOL cleanSync;
|
||||
|
||||
/// New client mode sent from server
|
||||
@property santa_clientmode_t newClientMode;
|
||||
/// Batch size for uploading events.
|
||||
@property NSUInteger eventBatchSize;
|
||||
|
||||
/// Batch size for uploading events, sent from server
|
||||
@property int32_t eventBatchSize;
|
||||
|
||||
/// Log upload URL sent from server
|
||||
/// Log upload URL sent from server. If set, LogUpload phase needs to happen.
|
||||
@property NSURL *uploadLogURL;
|
||||
|
||||
/// Rules downloaded from server
|
||||
/// Array of bundle paths to find binaries for.
|
||||
@property NSArray *bundleBinaryRequests;
|
||||
|
||||
/// Rules downloaded from server.
|
||||
@property NSMutableArray *downloadedRules;
|
||||
|
||||
@end
|
||||
@@ -55,9 +55,12 @@ static NSMutableDictionary *registeredCommands;
|
||||
+ (NSString *)helpForCommandWithName:(NSString *)commandName {
|
||||
Class<SNTCommand> command = registeredCommands[commandName];
|
||||
if (command) {
|
||||
NSString *shortHelp = [command shortHelpText];
|
||||
NSString *longHelp = [command longHelpText];
|
||||
if (longHelp) {
|
||||
return [NSString stringWithFormat:@"Help for '%@':\n%@", commandName, longHelp];
|
||||
} else if (shortHelp) {
|
||||
return [NSString stringWithFormat:@"Help for '%@':\n%@", commandName, shortHelp];
|
||||
} else {
|
||||
return @"This command does not have any help information.";
|
||||
}
|
||||
@@ -65,15 +68,16 @@ static NSMutableDictionary *registeredCommands;
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (SNTXPCConnection *)connectToDaemon {
|
||||
+ (SNTXPCConnection *)connectToDaemonRequired:(BOOL)required {
|
||||
SNTXPCConnection *daemonConn = [SNTXPCControlInterface configuredConnection];
|
||||
|
||||
daemonConn.invalidationHandler = ^{
|
||||
printf("An error occurred communicating with the daemon, is it running?\n");
|
||||
exit(1);
|
||||
};
|
||||
|
||||
[daemonConn resume];
|
||||
if (required) {
|
||||
daemonConn.invalidationHandler = ^{
|
||||
printf("An error occurred communicating with the daemon, is it running?\n");
|
||||
exit(1);
|
||||
};
|
||||
[daemonConn resume];
|
||||
}
|
||||
return daemonConn;
|
||||
}
|
||||
|
||||
@@ -89,11 +93,7 @@ static NSMutableDictionary *registeredCommands;
|
||||
exit(2);
|
||||
}
|
||||
|
||||
SNTXPCConnection *daemonConn;
|
||||
if ([command requiresDaemonConn]) {
|
||||
daemonConn = [self connectToDaemon];
|
||||
}
|
||||
|
||||
SNTXPCConnection *daemonConn = [self connectToDaemonRequired:[command requiresDaemonConn]];
|
||||
[command runWithArguments:arguments daemonConnection:daemonConn];
|
||||
|
||||
// The command is responsible for quitting.
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
///
|
||||
/// An authenticating NSURLSession, which can do both pinned verification of the SSL server
|
||||
/// and handle client certificate authentication from the keychain.
|
||||
///
|
||||
@interface SNTAuthenticatingURLSession : NSObject<NSURLSessionDelegate>
|
||||
|
||||
///
|
||||
/// The underlying session. Pass this session to NSURLRequest methods.
|
||||
///
|
||||
@property(readonly) NSURLSession *session;
|
||||
|
||||
///
|
||||
/// If set, this is the user-agent to send with requests, otherwise remains the default
|
||||
/// CFNetwork-based name.
|
||||
///
|
||||
@property(copy, nonatomic) NSString *userAgent;
|
||||
|
||||
///
|
||||
/// If set to YES, this session refuses redirect requests. Defaults to NO.
|
||||
///
|
||||
@property(nonatomic) BOOL refusesRedirects;
|
||||
|
||||
///
|
||||
/// If set, the server that we connect to _must_ match this string. Redirects to other
|
||||
/// hosts will not be allowed.
|
||||
///
|
||||
@property(copy, nonatomic) NSString *serverHostname;
|
||||
|
||||
///
|
||||
/// This should be PEM data containing one or more certificates to use to verify the server's
|
||||
/// certificate chain. This will override the trusted roots in the System Roots.
|
||||
///
|
||||
@property(copy, nonatomic) NSData *serverRootsPemData;
|
||||
|
||||
///
|
||||
/// If set and client certificate authentication is needed, the pkcs#12 file will be loaded
|
||||
///
|
||||
@property(copy, nonatomic) NSString *clientCertFile;
|
||||
|
||||
///
|
||||
/// If set and client certificate authentication is needed, the password being used for
|
||||
/// loading the clientCertFile
|
||||
///
|
||||
@property(copy, nonatomic) NSString *clientCertPassword;
|
||||
|
||||
///
|
||||
/// If set and client certificate authentication is needed, will search the keychain for a
|
||||
/// certificate matching this common name and use that for authentication
|
||||
/// @note Not case sensitive
|
||||
/// @note If multiple matching certificates are found, the first one is used.
|
||||
/// @note If this property is not set and neither is |clientCertIssuerCn|, the allowed issuers
|
||||
/// provided by the server will be used to find a matching certificate.
|
||||
///
|
||||
@property(copy, nonatomic) NSString *clientCertCommonName;
|
||||
|
||||
///
|
||||
/// If set and client certificate authentication is needed, will search the keychain for a
|
||||
/// certificate issued by an issuer with this name and use that for authentication.
|
||||
///
|
||||
/// @note Not case sensitive
|
||||
/// @note If multiple matching certificates are found, the first one is used.
|
||||
/// @note If this property is not set and neither is |clientCertCommonName|, the allowed issuers
|
||||
/// provided by the server will be used to find a matching certificate.
|
||||
///
|
||||
@property(copy, nonatomic) NSString *clientCertIssuerCn;
|
||||
|
||||
/// Designated initializer
|
||||
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration;
|
||||
|
||||
@end
|
||||
@@ -1,358 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTAuthenticatingURLSession.h"
|
||||
|
||||
#import "MOLCertificate.h"
|
||||
#import "SNTDERDecoder.h"
|
||||
#import "SNTLogging.h"
|
||||
|
||||
@implementation SNTAuthenticatingURLSession
|
||||
|
||||
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_session = [NSURLSession sessionWithConfiguration:configuration
|
||||
delegate:self
|
||||
delegateQueue:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
[config setTLSMinimumSupportedProtocol:kTLSProtocol12];
|
||||
[config setHTTPShouldUsePipelining:YES];
|
||||
return [self initWithSessionConfiguration:config];
|
||||
}
|
||||
|
||||
#pragma mark User Agent property
|
||||
|
||||
- (NSString *)userAgent {
|
||||
return _session.configuration.HTTPAdditionalHeaders[@"User-Agent"];
|
||||
}
|
||||
|
||||
- (void)setUserAgent:(NSString *)userAgent {
|
||||
NSMutableDictionary *addlHeaders = [_session.configuration.HTTPAdditionalHeaders mutableCopy];
|
||||
addlHeaders[@"User-Agent"] = userAgent;
|
||||
_session.configuration.HTTPAdditionalHeaders = addlHeaders;
|
||||
}
|
||||
|
||||
#pragma mark NSURLSessionDelegate methods
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
||||
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
|
||||
NSURLCredential *credential))completionHandler {
|
||||
NSURLProtectionSpace *protectionSpace = challenge.protectionSpace;
|
||||
|
||||
if (challenge.previousFailureCount > 0) {
|
||||
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.serverHostname && ![self.serverHostname isEqual:protectionSpace.host]) {
|
||||
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
if (![protectionSpace.protocol isEqual:NSURLProtectionSpaceHTTPS]) {
|
||||
LOGE(@"%@ is not a secure protocol", protectionSpace.protocol);
|
||||
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!protectionSpace.receivesCredentialSecurely) {
|
||||
LOGE(@"Secure authentication or protocol cannot be established.");
|
||||
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *authMethod = [protectionSpace authenticationMethod];
|
||||
|
||||
if (authMethod == NSURLAuthenticationMethodClientCertificate) {
|
||||
NSURLCredential *cred = [self clientCredentialForProtectionSpace:protectionSpace];
|
||||
if (cred) {
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
|
||||
return;
|
||||
} else {
|
||||
LOGE(@"Server asked for client authentication but no usable client certificate found.");
|
||||
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
||||
return;
|
||||
}
|
||||
} else if (authMethod == NSURLAuthenticationMethodServerTrust) {
|
||||
NSURLCredential *cred = [self serverCredentialForProtectionSpace:protectionSpace];
|
||||
if (cred) {
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
|
||||
return;
|
||||
} else {
|
||||
LOGE(@"Unable to verify server identity.");
|
||||
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Private Helpers for URLSession:didReceiveChallenge:completionHandler:
|
||||
|
||||
///
|
||||
/// Handles the process of locating a valid client certificate for authentication.
|
||||
/// Operates in one of four modes, depending on the configuration in config.plist
|
||||
///
|
||||
/// Mode 1: if syncClientAuthCertificateFile is set, use the identity in the pkcs file
|
||||
/// Mode 2: if syncClientAuthCertificateCn is set, look for an identity in the keychain with a
|
||||
/// matching common name and return it.
|
||||
/// Mode 3: if syncClientAuthCertificateIssuer is set, look for an identity in the keychain with a
|
||||
/// matching issuer common name and return it.
|
||||
/// Mode 4: use the list of issuer details sent down by the server to find an identity in the
|
||||
/// keychain.
|
||||
///
|
||||
/// If a valid identity cannot be found, returns nil.
|
||||
///
|
||||
- (NSURLCredential *)clientCredentialForProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
|
||||
__block OSStatus err = errSecSuccess;
|
||||
__block SecIdentityRef foundIdentity = NULL;
|
||||
|
||||
if (self.clientCertFile) {
|
||||
foundIdentity = [self identityFromFile:self.clientCertFile password:self.clientCertPassword];
|
||||
} else {
|
||||
CFArrayRef cfResults = NULL;
|
||||
SecItemCopyMatching((__bridge CFDictionaryRef) @{
|
||||
(id)kSecClass : (id)kSecClassCertificate,
|
||||
(id)kSecReturnRef : @YES,
|
||||
(id)kSecMatchLimit : (id)kSecMatchLimitAll
|
||||
}, (CFTypeRef *)&cfResults);
|
||||
NSArray *results = CFBridgingRelease(cfResults);
|
||||
|
||||
NSMutableArray *allCerts = [[MOLCertificate certificatesFromArray:results] mutableCopy];
|
||||
|
||||
if (self.clientCertCommonName) {
|
||||
foundIdentity = [self identityByFilteringArray:allCerts
|
||||
commonName:self.clientCertCommonName
|
||||
issuerCommonName:nil
|
||||
issuerCountryName:nil
|
||||
issuerOrgName:nil
|
||||
issuerOrgUnit:nil];
|
||||
} else if (self.clientCertIssuerCn) {
|
||||
foundIdentity = [self identityByFilteringArray:allCerts
|
||||
commonName:nil
|
||||
issuerCommonName:self.clientCertIssuerCn
|
||||
issuerCountryName:nil
|
||||
issuerOrgName:nil
|
||||
issuerOrgUnit:nil];
|
||||
} else {
|
||||
for (NSData *allowedIssuer in protectionSpace.distinguishedNames) {
|
||||
SNTDERDecoder *decoder = [[SNTDERDecoder alloc] initWithData:allowedIssuer];
|
||||
|
||||
if (!decoder) {
|
||||
LOGW(@"Unable to decode allowed distinguished name.");
|
||||
continue;
|
||||
}
|
||||
|
||||
foundIdentity = [self identityByFilteringArray:allCerts
|
||||
commonName:nil
|
||||
issuerCommonName:decoder.commonName
|
||||
issuerCountryName:decoder.countryName
|
||||
issuerOrgName:decoder.organizationName
|
||||
issuerOrgUnit:decoder.organizationalUnit];
|
||||
if (foundIdentity) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundIdentity) {
|
||||
SecCertificateRef certificate = NULL;
|
||||
err = SecIdentityCopyCertificate(foundIdentity, &certificate);
|
||||
MOLCertificate *clientCert = [[MOLCertificate alloc] initWithSecCertificateRef:certificate];
|
||||
LOGD(@"Client Trust: Valid client identity %@.", clientCert);
|
||||
NSURLCredential *cred =
|
||||
[NSURLCredential credentialWithIdentity:foundIdentity
|
||||
certificates:nil
|
||||
persistence:NSURLCredentialPersistenceForSession];
|
||||
return cred;
|
||||
} else {
|
||||
LOGD(@"Client Trust: No valid identity found.");
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Handles the process of evaluating the server's certificate chain.
|
||||
/// Operates in one of three modes, depending on the configuration in config.plist
|
||||
///
|
||||
/// Mode 1: if syncServerAuthRootsData is set, evaluates the server's certificate chain contains
|
||||
/// one of the certificates in the PEM data in the config plist.
|
||||
/// Mode 2: if syncServerAuthRootsFile is set, evaluates the server's certificate chain contains
|
||||
/// one of the certificates in the PEM data in the file specified.
|
||||
/// Mode 3: evaluates the server's certificate chain is trusted by the keychain.
|
||||
///
|
||||
/// If the server's certificate chain does not evaluate for any reason, returns nil.
|
||||
///
|
||||
- (NSURLCredential *)serverCredentialForProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
|
||||
SecTrustRef serverTrust = protectionSpace.serverTrust;
|
||||
if (serverTrust == NULL) {
|
||||
LOGD(@"Server Trust: No server trust information available");
|
||||
return nil;
|
||||
}
|
||||
|
||||
OSStatus err = errSecSuccess;
|
||||
|
||||
if (self.serverRootsPemData) {
|
||||
NSString *pemStrings = [[NSString alloc] initWithData:self.serverRootsPemData
|
||||
encoding:NSASCIIStringEncoding];
|
||||
NSArray *certs = [MOLCertificate certificatesFromPEM:pemStrings];
|
||||
|
||||
// Make a new array of the SecCertificateRef's from the MOLCertificate's.
|
||||
NSMutableArray *certRefs = [[NSMutableArray alloc] initWithCapacity:certs.count];
|
||||
for (MOLCertificate *cert in certs) {
|
||||
[certRefs addObject:(id)cert.certRef];
|
||||
}
|
||||
|
||||
// Set this array of certs as the anchors to trust.
|
||||
err = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)certRefs);
|
||||
if (err != errSecSuccess) {
|
||||
LOGD(@"Server Trust: Could not set anchor certificates: %d", err);
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the server's cert chain.
|
||||
SecTrustResultType result = kSecTrustResultInvalid;
|
||||
err = SecTrustEvaluate(serverTrust, &result);
|
||||
if (err != errSecSuccess) {
|
||||
LOGD(@"Server Trust: Unable to evaluate certificate chain for server: %d", err);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Print details about the server's leaf certificate.
|
||||
SecCertificateRef firstCert = SecTrustGetCertificateAtIndex(serverTrust, 0);
|
||||
if (firstCert) {
|
||||
MOLCertificate *cert = [[MOLCertificate alloc] initWithSecCertificateRef:firstCert];
|
||||
LOGD(@"Server Trust: Server leaf cert: %@", cert);
|
||||
}
|
||||
|
||||
// Having a trust level "unspecified" by the user is the usual result, described at
|
||||
// https://developer.apple.com/library/mac/qa/qa1360
|
||||
if (result != kSecTrustResultProceed && result != kSecTrustResultUnspecified) {
|
||||
LOGD(@"Server Trust: Server isn't trusted. SecTrustResultType: %d", result);
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSURLCredential credentialForTrust:serverTrust];
|
||||
}
|
||||
|
||||
/**
|
||||
Given an array of MOLCertificate objects and some properties, filter the array
|
||||
repeatedly until an identity is found that fulfills the signing chain.
|
||||
*/
|
||||
- (SecIdentityRef)identityByFilteringArray:(NSArray *)array
|
||||
commonName:(NSString *)commonName
|
||||
issuerCommonName:(NSString *)issuerCommonName
|
||||
issuerCountryName:(NSString *)issuerCountryName
|
||||
issuerOrgName:(NSString *)issuerOrgName
|
||||
issuerOrgUnit:(NSString *)issuerOrgUnit {
|
||||
NSMutableArray *predicates = [NSMutableArray arrayWithCapacity:4];
|
||||
|
||||
if (commonName) {
|
||||
[predicates addObject:[NSPredicate predicateWithFormat:@"SELF.commonName == %@",
|
||||
commonName]];
|
||||
}
|
||||
if (issuerCommonName) {
|
||||
[predicates addObject:[NSPredicate predicateWithFormat:@"SELF.issuerCommonName == %@",
|
||||
issuerCommonName]];
|
||||
}
|
||||
if (issuerCountryName) {
|
||||
[predicates addObject:[NSPredicate predicateWithFormat:@"SELF.issuerCountryName == %@",
|
||||
issuerCountryName]];
|
||||
}
|
||||
if (issuerOrgName) {
|
||||
[predicates addObject:[NSPredicate predicateWithFormat:@"SELF.issuerOrgName == %@",
|
||||
issuerOrgName]];
|
||||
}
|
||||
if (issuerOrgUnit) {
|
||||
[predicates addObject:[NSPredicate predicateWithFormat:@"SELF.issuerOrgUnit == %@",
|
||||
issuerOrgUnit]];
|
||||
}
|
||||
|
||||
NSCompoundPredicate *andPreds = [NSCompoundPredicate andPredicateWithSubpredicates:predicates];
|
||||
|
||||
NSArray *filteredCerts = [array filteredArrayUsingPredicate:andPreds];
|
||||
if (!filteredCerts.count) return NULL;
|
||||
|
||||
for (MOLCertificate *cert in filteredCerts) {
|
||||
SecIdentityRef identityRef = NULL;
|
||||
OSStatus status = SecIdentityCreateWithCertificate(NULL, cert.certRef, &identityRef);
|
||||
if (status == errSecSuccess) {
|
||||
return identityRef;
|
||||
} else {
|
||||
// Avoid infinite recursion from self-signed certs
|
||||
if ((cert.commonName && [cert.commonName isEqual:cert.issuerCommonName]) &&
|
||||
(cert.countryName && [cert.countryName isEqual:cert.issuerCountryName]) &&
|
||||
(cert.orgName && [cert.orgName isEqual:cert.issuerOrgName]) &&
|
||||
(cert.orgUnit && [cert.orgUnit isEqual:cert.issuerOrgUnit])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return [self identityByFilteringArray:array
|
||||
commonName:nil
|
||||
issuerCommonName:cert.commonName
|
||||
issuerCountryName:cert.countryName
|
||||
issuerOrgName:cert.orgName
|
||||
issuerOrgUnit:cert.orgUnit];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
- (SecIdentityRef)identityFromFile:(NSString *)file password:(NSString *)password {
|
||||
NSError *error;
|
||||
NSData *data = [NSData dataWithContentsOfFile:file options:0 error:&error];
|
||||
if (error) {
|
||||
LOGD(@"Client Trust: Couldn't open client certificate %@: %@",
|
||||
self.clientCertFile,
|
||||
[error localizedDescription]);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *options = (password ? @{(__bridge id)kSecImportExportPassphrase : password} : @{});
|
||||
CFArrayRef cfIdentities;
|
||||
OSStatus err = SecPKCS12Import(
|
||||
(__bridge CFDataRef)data, (__bridge CFDictionaryRef)options, &cfIdentities);
|
||||
NSArray *identities = CFBridgingRelease(cfIdentities);
|
||||
|
||||
if (err != errSecSuccess) {
|
||||
LOGD(@"Client Trust: Couldn't load client certificate %@: %d", self.clientCertFile, err);
|
||||
return nil;
|
||||
}
|
||||
|
||||
return (__bridge SecIdentityRef)identities[0][(__bridge id)kSecImportItemIdentity];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,31 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
@class SNTCommandSyncState;
|
||||
@class SNTXPCConnection;
|
||||
|
||||
@interface SNTCommandSyncEventUpload : NSObject
|
||||
|
||||
+ (void)performSyncInSession:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler;
|
||||
|
||||
+ (void)uploadSingleEventWithSHA256:(NSString *)SHA256
|
||||
session:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler;
|
||||
|
||||
@end
|
||||
@@ -1,275 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommandSyncEventUpload.h"
|
||||
|
||||
#include "SNTLogging.h"
|
||||
|
||||
#import "MOLCertificate.h"
|
||||
#import "MOLCodesignChecker.h"
|
||||
#import "NSData+Zlib.h"
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
#import "SNTCommandSyncState.h"
|
||||
#import "SNTFileInfo.h"
|
||||
#import "SNTStoredEvent.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@implementation SNTCommandSyncEventUpload
|
||||
|
||||
+ (void)performSyncInSession:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler {
|
||||
NSURL *url = [NSURL URLWithString:[kURLEventUpload stringByAppendingString:syncState.machineID]
|
||||
relativeToURL:syncState.syncBaseURL];
|
||||
|
||||
[[daemonConn remoteObjectProxy] databaseEventsPending:^(NSArray *events) {
|
||||
if ([events count] == 0) {
|
||||
handler(YES);
|
||||
} else {
|
||||
[self uploadEventsFromArray:events
|
||||
toURL:url
|
||||
inSession:session
|
||||
batchSize:syncState.eventBatchSize
|
||||
daemonConn:daemonConn
|
||||
completionHandler:handler];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)uploadSingleEventWithSHA256:(NSString *)SHA256
|
||||
session:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler {
|
||||
NSURL *url = [NSURL URLWithString:[kURLEventUpload stringByAppendingString:syncState.machineID]
|
||||
relativeToURL:syncState.syncBaseURL];
|
||||
[[daemonConn remoteObjectProxy] databaseEventForSHA256:SHA256 reply:^(SNTStoredEvent *event) {
|
||||
if (!event) {
|
||||
handler(YES);
|
||||
return;
|
||||
}
|
||||
|
||||
[self uploadEventsFromArray:@[ event ]
|
||||
toURL:url
|
||||
inSession:session
|
||||
batchSize:1
|
||||
daemonConn:daemonConn
|
||||
completionHandler:handler];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)uploadEventsFromArray:(NSArray *)events
|
||||
toURL:(NSURL *)url
|
||||
inSession:(NSURLSession *)session
|
||||
batchSize:(NSUInteger)batchSize
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler {
|
||||
NSMutableArray *uploadEvents = [[NSMutableArray alloc] init];
|
||||
|
||||
NSMutableArray *eventIds = [NSMutableArray arrayWithCapacity:events.count];
|
||||
for (SNTStoredEvent *event in events) {
|
||||
[uploadEvents addObject:[self dictionaryForEvent:event]];
|
||||
[eventIds addObject:event.idx];
|
||||
|
||||
if (event.fileBundleID) {
|
||||
NSArray *relatedBinaries = [self findRelatedBinaries:event];
|
||||
[uploadEvents addObjectsFromArray:relatedBinaries];
|
||||
}
|
||||
|
||||
if (eventIds.count >= batchSize) break;
|
||||
}
|
||||
|
||||
NSDictionary *uploadReq = @{kEvents : uploadEvents};
|
||||
|
||||
NSData *requestBody;
|
||||
@try {
|
||||
requestBody = [NSJSONSerialization dataWithJSONObject:uploadReq options:0 error:nil];
|
||||
} @catch (NSException *exception) {
|
||||
LOGE(@"Failed to parse event(s) into JSON");
|
||||
LOGD(@"Parsing error: %@", [exception reason]);
|
||||
}
|
||||
|
||||
NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:url];
|
||||
[req setHTTPMethod:@"POST"];
|
||||
[req setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||
|
||||
NSData *compressed = [requestBody zlibCompressed];
|
||||
if (compressed) {
|
||||
requestBody = compressed;
|
||||
[req setValue:@"zlib" forHTTPHeaderField:@"Content-Encoding"];
|
||||
}
|
||||
|
||||
[req setHTTPBody:requestBody];
|
||||
|
||||
[[session dataTaskWithRequest:req completionHandler:^(NSData *data,
|
||||
NSURLResponse *response,
|
||||
NSError *error) {
|
||||
long statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if (statusCode != 200) {
|
||||
LOGE(@"HTTP Response: %ld %@",
|
||||
statusCode,
|
||||
[[NSHTTPURLResponse localizedStringForStatusCode:statusCode] capitalizedString]);
|
||||
LOGD(@"%@", error);
|
||||
handler(NO);
|
||||
} else {
|
||||
LOGI(@"Uploaded %lu events", eventIds.count);
|
||||
|
||||
[[daemonConn remoteObjectProxy] databaseRemoveEventsWithIDs:eventIds];
|
||||
|
||||
NSArray *nextEvents = [events subarrayWithRange:NSMakeRange(eventIds.count,
|
||||
events.count - eventIds.count)];
|
||||
if (nextEvents.count == 0) {
|
||||
handler(YES);
|
||||
} else {
|
||||
[self uploadEventsFromArray:nextEvents
|
||||
toURL:url
|
||||
inSession:session
|
||||
batchSize:batchSize
|
||||
daemonConn:daemonConn
|
||||
completionHandler:handler];
|
||||
}
|
||||
}
|
||||
}] resume];
|
||||
}
|
||||
|
||||
+ (NSDictionary *)dictionaryForEvent:(SNTStoredEvent *)event {
|
||||
#define ADDKEY(dict, key, value) if (value) dict[key] = value
|
||||
NSMutableDictionary *newEvent = [NSMutableDictionary dictionary];
|
||||
|
||||
ADDKEY(newEvent, kFileSHA256, event.fileSHA256);
|
||||
ADDKEY(newEvent, kFilePath, [event.filePath stringByDeletingLastPathComponent]);
|
||||
ADDKEY(newEvent, kFileName, [event.filePath lastPathComponent]);
|
||||
ADDKEY(newEvent, kExecutingUser, event.executingUser);
|
||||
ADDKEY(newEvent, kExecutionTime, @([event.occurrenceDate timeIntervalSince1970]));
|
||||
ADDKEY(newEvent, kLoggedInUsers, event.loggedInUsers);
|
||||
ADDKEY(newEvent, kCurrentSessions, event.currentSessions);
|
||||
|
||||
switch (event.decision) {
|
||||
case EVENTSTATE_ALLOW_UNKNOWN: ADDKEY(newEvent, kDecision, kDecisionAllowUnknown); break;
|
||||
case EVENTSTATE_ALLOW_BINARY: ADDKEY(newEvent, kDecision, kDecisionAllowBinary); break;
|
||||
case EVENTSTATE_ALLOW_CERTIFICATE:
|
||||
ADDKEY(newEvent, kDecision, kDecisionAllowCertificate);
|
||||
break;
|
||||
case EVENTSTATE_ALLOW_SCOPE: ADDKEY(newEvent, kDecision, kDecisionAllowScope); break;
|
||||
case EVENTSTATE_BLOCK_UNKNOWN: ADDKEY(newEvent, kDecision, kDecisionBlockUnknown); break;
|
||||
case EVENTSTATE_BLOCK_BINARY: ADDKEY(newEvent, kDecision, kDecisionBlockBinary); break;
|
||||
case EVENTSTATE_BLOCK_CERTIFICATE:
|
||||
ADDKEY(newEvent, kDecision, kDecisionBlockCertificate);
|
||||
break;
|
||||
case EVENTSTATE_BLOCK_SCOPE: ADDKEY(newEvent, kDecision, kDecisionBlockScope); break;
|
||||
case EVENTSTATE_RELATED_BINARY: ADDKEY(newEvent, kDecision, kDecisionRelatedBinary); break;
|
||||
default: ADDKEY(newEvent, kDecision, kDecisionUnknown);
|
||||
}
|
||||
|
||||
ADDKEY(newEvent, kFileBundleID, event.fileBundleID);
|
||||
ADDKEY(newEvent, kFileBundleName, event.fileBundleName);
|
||||
ADDKEY(newEvent, kFileBundleVersion, event.fileBundleVersion);
|
||||
ADDKEY(newEvent, kFileBundleShortVersionString, event.fileBundleVersionString);
|
||||
|
||||
ADDKEY(newEvent, kPID, event.pid);
|
||||
ADDKEY(newEvent, kPPID, event.ppid);
|
||||
ADDKEY(newEvent, kParentName, event.parentName);
|
||||
|
||||
ADDKEY(newEvent, kQuarantineDataURL, event.quarantineDataURL);
|
||||
ADDKEY(newEvent, kQuarantineRefererURL, event.quarantineRefererURL);
|
||||
ADDKEY(newEvent, kQuarantineTimestamp, @([event.quarantineTimestamp timeIntervalSince1970]));
|
||||
ADDKEY(newEvent, kQuarantineAgentBundleID, event.quarantineAgentBundleID);
|
||||
|
||||
NSMutableArray *signingChain = [NSMutableArray arrayWithCapacity:event.signingChain.count];
|
||||
for (NSUInteger i = 0; i < event.signingChain.count; ++i) {
|
||||
MOLCertificate *cert = [event.signingChain objectAtIndex:i];
|
||||
|
||||
NSMutableDictionary *certDict = [NSMutableDictionary dictionary];
|
||||
ADDKEY(certDict, kCertSHA256, cert.SHA256);
|
||||
ADDKEY(certDict, kCertCN, cert.commonName);
|
||||
ADDKEY(certDict, kCertOrg, cert.orgName);
|
||||
ADDKEY(certDict, kCertOU, cert.orgUnit);
|
||||
ADDKEY(certDict, kCertValidFrom, @([cert.validFrom timeIntervalSince1970]));
|
||||
ADDKEY(certDict, kCertValidUntil, @([cert.validUntil timeIntervalSince1970]));
|
||||
|
||||
[signingChain addObject:certDict];
|
||||
}
|
||||
newEvent[kSigningChain] = signingChain;
|
||||
|
||||
return newEvent;
|
||||
#undef ADDKEY
|
||||
}
|
||||
|
||||
+ (NSArray *)findRelatedBinaries:(SNTStoredEvent *)event {
|
||||
// Prevent processing the same bundle twice.
|
||||
static NSMutableDictionary *previouslyProcessedBundles;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
previouslyProcessedBundles = [NSMutableDictionary dictionary];
|
||||
});
|
||||
if (previouslyProcessedBundles[event.fileBundleID]) return nil;
|
||||
previouslyProcessedBundles[event.fileBundleID] = @YES;
|
||||
|
||||
NSMutableArray *relatedEvents = [NSMutableArray array];
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
__block BOOL shouldCancel = NO;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
||||
SNTFileInfo *originalFile = [[SNTFileInfo alloc] initWithPath:event.filePath];
|
||||
NSString *bundlePath = originalFile.bundlePath;
|
||||
originalFile = nil; // release originalFile early.
|
||||
|
||||
NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:bundlePath];
|
||||
NSString *file;
|
||||
|
||||
while (file = [dirEnum nextObject]) {
|
||||
@autoreleasepool {
|
||||
if (shouldCancel) break;
|
||||
if ([dirEnum fileAttributes][NSFileType] != NSFileTypeRegular) continue;
|
||||
|
||||
file = [bundlePath stringByAppendingPathComponent:file];
|
||||
|
||||
// Don't record the binary that triggered this event as a related binary.
|
||||
if ([file isEqual:event.filePath]) continue;
|
||||
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:file];
|
||||
if (fi.isExecutable) {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
se.filePath = fi.path;
|
||||
se.fileSHA256 = fi.SHA256;
|
||||
se.decision = EVENTSTATE_RELATED_BINARY;
|
||||
se.fileBundleID = event.fileBundleID;
|
||||
se.fileBundleName = event.fileBundleName;
|
||||
se.fileBundleVersion = event.fileBundleVersion;
|
||||
se.fileBundleVersionString = event.fileBundleVersionString;
|
||||
|
||||
MOLCodesignChecker *cs = [[MOLCodesignChecker alloc] initWithBinaryPath:se.filePath];
|
||||
se.signingChain = cs.certificates;
|
||||
|
||||
[relatedEvents addObject:[self dictionaryForEvent:se]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_semaphore_signal(sema);
|
||||
});
|
||||
|
||||
// Give the search up to 5s per event to run.
|
||||
// This might need tweaking if it seems to slow down syncing or misses too much to be useful.
|
||||
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
|
||||
shouldCancel = YES;
|
||||
LOGD(@"Timed out while searching for related events. Bundle ID: %@", event.fileBundleID);
|
||||
}
|
||||
|
||||
return relatedEvents;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,70 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommandSyncPostflight.h"
|
||||
|
||||
#include "SNTLogging.h"
|
||||
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
#import "SNTCommandSyncState.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@implementation SNTCommandSyncPostflight
|
||||
|
||||
+ (void)performSyncInSession:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler {
|
||||
NSURL *url = [NSURL URLWithString:[kURLPostflight stringByAppendingString:syncState.machineID]
|
||||
relativeToURL:syncState.syncBaseURL];
|
||||
NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:url];
|
||||
[req setHTTPMethod:@"POST"];
|
||||
|
||||
[[session dataTaskWithRequest:req completionHandler:^(NSData *data,
|
||||
NSURLResponse *response,
|
||||
NSError *error) {
|
||||
long statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if (statusCode != 200) {
|
||||
LOGE(@"HTTP Response: %ld %@",
|
||||
statusCode,
|
||||
[[NSHTTPURLResponse localizedStringForStatusCode:statusCode] capitalizedString]);
|
||||
LOGD(@"%@", error);
|
||||
handler(NO);
|
||||
} else {
|
||||
NSDictionary *r = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
|
||||
if (syncState.newClientMode) {
|
||||
[[daemonConn remoteObjectProxy] setClientMode:syncState.newClientMode reply:^{}];
|
||||
}
|
||||
|
||||
NSString *backoffInterval = r[kBackoffInterval];
|
||||
if (backoffInterval) {
|
||||
[[daemonConn remoteObjectProxy] setNextSyncInterval:[backoffInterval intValue] reply:^{}];
|
||||
}
|
||||
|
||||
if (syncState.cleanSync) {
|
||||
[[daemonConn remoteObjectProxy] setSyncCleanRequired:NO reply:^{}];
|
||||
}
|
||||
|
||||
// Update last sync success
|
||||
[[daemonConn remoteObjectProxy] setSyncLastSuccess:[NSDate date] reply:^{}];
|
||||
|
||||
handler(YES);
|
||||
}
|
||||
}] resume];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -1,106 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommandSyncPreflight.h"
|
||||
|
||||
#include "SNTKernelCommon.h"
|
||||
#include "SNTLogging.h"
|
||||
|
||||
#import "NSData+Zlib.h"
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
#import "SNTCommandSyncState.h"
|
||||
#import "SNTConfigurator.h"
|
||||
#import "SNTSystemInfo.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@implementation SNTCommandSyncPreflight
|
||||
|
||||
+ (void)performSyncInSession:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler {
|
||||
NSURL *url = [NSURL URLWithString:[kURLPreflight stringByAppendingString:syncState.machineID]
|
||||
relativeToURL:syncState.syncBaseURL];
|
||||
|
||||
NSMutableDictionary *requestDict = [NSMutableDictionary dictionary];
|
||||
requestDict[kSerialNumber] = [SNTSystemInfo serialNumber];
|
||||
requestDict[kHostname] = [SNTSystemInfo shortHostname];
|
||||
requestDict[kSantaVer] = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
||||
requestDict[kOSVer] = [SNTSystemInfo osVersion];
|
||||
requestDict[kOSBuild] = [SNTSystemInfo osBuild];
|
||||
requestDict[kPrimaryUser] = syncState.machineOwner;
|
||||
|
||||
// If user requested it or we've never had a successful sync, try from a clean slate.
|
||||
if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--clean"] ||
|
||||
[[SNTConfigurator configurator] syncCleanRequired]) {
|
||||
requestDict[kRequestCleanSync] = @YES;
|
||||
}
|
||||
|
||||
NSData *requestBody = [NSJSONSerialization dataWithJSONObject:requestDict
|
||||
options:0
|
||||
error:nil];
|
||||
|
||||
NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:url];
|
||||
[req setHTTPMethod:@"POST"];
|
||||
[req setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||
|
||||
NSData *compressed = [requestBody zlibCompressed];
|
||||
if (compressed) {
|
||||
requestBody = compressed;
|
||||
[req setValue:@"zlib" forHTTPHeaderField:@"Content-Encoding"];
|
||||
}
|
||||
|
||||
[req setHTTPBody:requestBody];
|
||||
|
||||
[[session dataTaskWithRequest:req completionHandler:^(NSData *data,
|
||||
NSURLResponse *response,
|
||||
NSError *error) {
|
||||
long statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if (statusCode != 200) {
|
||||
LOGE(@"HTTP Response: %ld %@",
|
||||
statusCode,
|
||||
[[NSHTTPURLResponse localizedStringForStatusCode:statusCode] capitalizedString]);
|
||||
LOGD(@"%@", error);
|
||||
handler(NO);
|
||||
} else {
|
||||
NSDictionary *r = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
|
||||
syncState.eventBatchSize = [r[kBatchSize] intValue];
|
||||
syncState.uploadLogURL = [NSURL URLWithString:r[kUploadLogsURL]];
|
||||
|
||||
if ([r[kClientMode] isEqual:kClientModeMonitor]) {
|
||||
syncState.newClientMode = CLIENTMODE_MONITOR;
|
||||
} else if ([r[kClientMode] isEqual:kClientModeLockdown]) {
|
||||
syncState.newClientMode = CLIENTMODE_LOCKDOWN;
|
||||
}
|
||||
|
||||
if ([r[kWhitelistRegex] isKindOfClass:[NSString class]]) {
|
||||
[[daemonConn remoteObjectProxy] setWhitelistPathRegex:r[kWhitelistRegex] reply:^{}];
|
||||
}
|
||||
|
||||
if ([r[kBlacklistRegex] isKindOfClass:[NSString class]]) {
|
||||
[[daemonConn remoteObjectProxy] setBlacklistPathRegex:r[kBlacklistRegex] reply:^{}];
|
||||
}
|
||||
|
||||
if ([r[kCleanSync] boolValue]) {
|
||||
syncState.cleanSync = YES;
|
||||
}
|
||||
|
||||
handler(YES);
|
||||
}
|
||||
}] resume];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,25 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
@class SNTCommandSyncState;
|
||||
@class SNTXPCConnection;
|
||||
|
||||
@interface SNTCommandSyncRuleDownload : NSObject
|
||||
|
||||
+ (void)performSyncInSession:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler;
|
||||
|
||||
@end
|
||||
@@ -1,148 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommandSyncRuleDownload.h"
|
||||
|
||||
#import "SNTCommandSyncConstants.h"
|
||||
#import "SNTCommandSyncState.h"
|
||||
#import "SNTRule.h"
|
||||
#import "SNTXPCConnection.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
#include "SNTLogging.h"
|
||||
|
||||
@implementation SNTCommandSyncRuleDownload
|
||||
|
||||
+ (void)performSyncInSession:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler {
|
||||
NSURL *url = [NSURL URLWithString:[kURLRuleDownload stringByAppendingString:syncState.machineID]
|
||||
relativeToURL:syncState.syncBaseURL];
|
||||
[self ruleDownloadWithCursor:nil
|
||||
url:url
|
||||
session:session
|
||||
syncState:syncState
|
||||
daemonConn:daemonConn
|
||||
completionHandler:handler];
|
||||
}
|
||||
|
||||
+ (void)ruleDownloadWithCursor:(NSString *)cursor
|
||||
url:(NSURL *)url
|
||||
session:(NSURLSession *)session
|
||||
syncState:(SNTCommandSyncState *)syncState
|
||||
daemonConn:(SNTXPCConnection *)daemonConn
|
||||
completionHandler:(void (^)(BOOL success))handler {
|
||||
NSDictionary *requestDict = (cursor ? @{kCursor : cursor} : @{});
|
||||
|
||||
if (!syncState.downloadedRules) {
|
||||
syncState.downloadedRules = [NSMutableArray array];
|
||||
}
|
||||
|
||||
NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:url];
|
||||
[req setHTTPBody:[NSJSONSerialization dataWithJSONObject:requestDict
|
||||
options:0
|
||||
error:nil]];
|
||||
[req setHTTPMethod:@"POST"];
|
||||
[req setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||
[[session dataTaskWithRequest:req completionHandler:^(NSData *data,
|
||||
NSURLResponse *response,
|
||||
NSError *error) {
|
||||
long statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
if (statusCode != 200) {
|
||||
LOGE(@"HTTP Response: %ld %@",
|
||||
statusCode,
|
||||
[[NSHTTPURLResponse localizedStringForStatusCode:statusCode] capitalizedString]);
|
||||
LOGD(@"%@", error);
|
||||
handler(NO);
|
||||
} else {
|
||||
NSDictionary *resp = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
if (!resp) {
|
||||
LOGE(@"Failed to decode server's response");
|
||||
handler(NO);
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray *receivedRules = resp[kRules];
|
||||
for (NSDictionary *rule in receivedRules) {
|
||||
SNTRule *r = [self ruleFromDictionary:rule];
|
||||
if (r) [syncState.downloadedRules addObject:r];
|
||||
}
|
||||
|
||||
if (resp[kCursor]) {
|
||||
[self ruleDownloadWithCursor:resp[kCursor]
|
||||
url:url
|
||||
session:session
|
||||
syncState:syncState
|
||||
daemonConn:daemonConn
|
||||
completionHandler:handler];
|
||||
} else {
|
||||
if (syncState.downloadedRules.count) {
|
||||
[[daemonConn remoteObjectProxy] databaseRuleAddRules:syncState.downloadedRules
|
||||
cleanSlate:syncState.cleanSync
|
||||
reply:^(BOOL success) {
|
||||
if (success) {
|
||||
LOGI(@"Added %lu rule(s)", syncState.downloadedRules.count);
|
||||
handler(YES);
|
||||
} else {
|
||||
LOGE(@"Failed to add rules to database");
|
||||
handler(NO);
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
handler(YES);
|
||||
}
|
||||
}
|
||||
}
|
||||
}] resume];
|
||||
}
|
||||
|
||||
+ (SNTRule *)ruleFromDictionary:(NSDictionary *)dict {
|
||||
if (![dict isKindOfClass:[NSDictionary class]]) return nil;
|
||||
|
||||
SNTRule *newRule = [[SNTRule alloc] init];
|
||||
newRule.shasum = dict[kRuleSHA256];
|
||||
if (newRule.shasum.length != 64) return nil;
|
||||
|
||||
NSString *policyString = dict[kRulePolicy];
|
||||
if ([policyString isEqual:kRulePolicyWhitelist]) {
|
||||
newRule.state = RULESTATE_WHITELIST;
|
||||
} else if ([policyString isEqual:kRulePolicyBlacklist]) {
|
||||
newRule.state = RULESTATE_BLACKLIST;
|
||||
} else if ([policyString isEqual:kRulePolicySilentBlacklist]) {
|
||||
newRule.state = RULESTATE_SILENT_BLACKLIST;
|
||||
} else if ([policyString isEqual:kRulePolicyRemove]) {
|
||||
newRule.state = RULESTATE_REMOVE;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *ruleTypeString = dict[kRuleType];
|
||||
if ([ruleTypeString isEqual:kRuleTypeBinary]) {
|
||||
newRule.type = RULETYPE_BINARY;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeCertificate]) {
|
||||
newRule.type = RULETYPE_CERT;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *customMsg = dict[kRuleCustomMsg];
|
||||
if (customMsg.length) {
|
||||
newRule.customMsg = customMsg;
|
||||
}
|
||||
|
||||
return newRule;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,35 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
///
|
||||
/// This is a simple ASN.1 decoder that utilizes Apple's SecAsn1Decode
|
||||
/// to parse the @c distinguishedNames property of NSURLProtectionSpace.
|
||||
///
|
||||
@interface SNTDERDecoder : NSObject
|
||||
|
||||
@property(readonly) NSString *commonName;
|
||||
@property(readonly) NSString *organizationName;
|
||||
@property(readonly) NSString *organizationalUnit;
|
||||
@property(readonly) NSString *countryName;
|
||||
|
||||
///
|
||||
/// Designated initializer.
|
||||
///
|
||||
/// @param data one of the objects in the
|
||||
/// NSURLProtectionSpace.distinguishedNames array
|
||||
/// @return nil if decoding fails to find any expected objects
|
||||
///
|
||||
- (instancetype)initWithData:(NSData *)data;
|
||||
|
||||
@end
|
||||
@@ -1,217 +0,0 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTDERDecoder.h"
|
||||
|
||||
#import <Security/SecAsn1Coder.h>
|
||||
#import <Security/SecAsn1Templates.h>
|
||||
|
||||
@interface SNTDERDecoder ()
|
||||
@property NSDictionary *decodedObjects;
|
||||
@end
|
||||
|
||||
@implementation SNTDERDecoder
|
||||
|
||||
#pragma mark Init
|
||||
|
||||
- (instancetype)initWithData:(NSData *)data {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
if (!data) return nil;
|
||||
|
||||
_decodedObjects = [self decodeData:data];
|
||||
if (!_decodedObjects || [_decodedObjects count] == 0) return nil;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"/C=%@/O=%@/OU=%@/CN=%@",
|
||||
self.countryName,
|
||||
self.organizationName,
|
||||
self.organizationalUnit,
|
||||
self.commonName];
|
||||
}
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
- (NSString *)commonName {
|
||||
return self.decodedObjects[(__bridge id)kSecOIDCommonName];
|
||||
}
|
||||
|
||||
- (NSString *)organizationName {
|
||||
return self.decodedObjects[(__bridge id)kSecOIDOrganizationName];
|
||||
}
|
||||
|
||||
- (NSString *)organizationalUnit {
|
||||
return self.decodedObjects[(__bridge id)kSecOIDOrganizationalUnitName];
|
||||
}
|
||||
|
||||
- (NSString *)countryName {
|
||||
return self.decodedObjects[(__bridge id)kSecOIDCountryName];
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
/**
|
||||
* The DER data provided by NSURLProtectionSpace.distinguishedNames looks like
|
||||
* this:
|
||||
*
|
||||
* SEQUENCE {
|
||||
* SET {
|
||||
* SEQUENCE {
|
||||
* OBJECT IDENTIFIER (2 5 4 6)
|
||||
* PrintableString 'US'
|
||||
* }
|
||||
* }
|
||||
* SET {
|
||||
* SEQUENCE {
|
||||
* OBJECT IDENTIFIER (2 5 4 10)
|
||||
* PrintableString 'Megaco Inc'
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* This method assumes the passed in data will be in that format. If it isn't,
|
||||
* the DER decoding will fail and this method will return nil.
|
||||
**/
|
||||
- (NSDictionary *)decodeData:(NSData *)data {
|
||||
typedef struct {
|
||||
SecAsn1Oid oid;
|
||||
SecAsn1Item value;
|
||||
} OIDKeyValue;
|
||||
|
||||
static const SecAsn1Template kOIDValueTemplate[] = {
|
||||
{SEC_ASN1_SEQUENCE, 0, NULL, sizeof(OIDKeyValue)},
|
||||
{SEC_ASN1_OBJECT_ID, offsetof(OIDKeyValue, oid), NULL, 0},
|
||||
{SEC_ASN1_ANY_CONTENTS, offsetof(OIDKeyValue, value), NULL, 0},
|
||||
{0, 0, NULL, 0}};
|
||||
|
||||
typedef struct {
|
||||
OIDKeyValue **vals;
|
||||
} OIDKeyValueList;
|
||||
|
||||
static const SecAsn1Template kSetOfOIDValueTemplate[] = {
|
||||
{SEC_ASN1_SET_OF, 0, kOIDValueTemplate, sizeof(OIDKeyValueList)},
|
||||
{0, 0, NULL, 0}};
|
||||
|
||||
typedef struct {
|
||||
OIDKeyValueList **lists;
|
||||
} OIDKeyValueListSeq;
|
||||
|
||||
static const SecAsn1Template kSequenceOfSetOfOIDValueTemplate[] = {
|
||||
{SEC_ASN1_SEQUENCE_OF, 0, kSetOfOIDValueTemplate, sizeof(OIDKeyValueListSeq)},
|
||||
{0, 0, NULL, 0}};
|
||||
|
||||
OSStatus err = errSecSuccess;
|
||||
SecAsn1CoderRef coder;
|
||||
|
||||
err = SecAsn1CoderCreate(&coder);
|
||||
if (err != errSecSuccess) return nil;
|
||||
|
||||
OIDKeyValueListSeq a;
|
||||
err = SecAsn1Decode(coder,
|
||||
data.bytes,
|
||||
data.length,
|
||||
kSequenceOfSetOfOIDValueTemplate,
|
||||
&a);
|
||||
if (err != errSecSuccess) {
|
||||
SecAsn1CoderRelease(coder);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// The data is decoded but now it's in a number of embedded structs.
|
||||
// Massage that into a nice dictionary of OID->String pairs.
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
OIDKeyValueList *anAttr;
|
||||
for (NSUInteger i = 0; (anAttr = a.lists[i]); ++i) {
|
||||
OIDKeyValue *keyValue = anAttr->vals[0];
|
||||
|
||||
// Sanity check
|
||||
if (keyValue->value.Length > data.length) {
|
||||
SecAsn1CoderRelease(coder);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Get the string value. First try creating as a UTF-8 string. If that fails,
|
||||
// fallback to trying as an ASCII string. If it still doesn't work, continue on
|
||||
// to the next value.
|
||||
NSString *valueString;
|
||||
valueString = [[NSString alloc] initWithBytes:keyValue->value.Data
|
||||
length:keyValue->value.Length
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if (!valueString) {
|
||||
valueString = [[NSString alloc] initWithBytes:keyValue->value.Data
|
||||
length:keyValue->value.Length
|
||||
encoding:NSASCIIStringEncoding];
|
||||
}
|
||||
if (!valueString) continue;
|
||||
|
||||
// The OID is still encoded, so we need to decode it.
|
||||
NSString *objectId = [SNTDERDecoder decodeOIDWithBytes:keyValue->oid.Data
|
||||
length:keyValue->oid.Length];
|
||||
|
||||
// Add to the dictionary
|
||||
dict[objectId] = valueString;
|
||||
}
|
||||
|
||||
SecAsn1CoderRelease(coder);
|
||||
return dict;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes an ASN.1 Object Identifier into a string separated by periods.
|
||||
* See http://msdn.microsoft.com/en-us/library/bb540809(v=vs.85).aspx for
|
||||
* details of the encoding.
|
||||
**/
|
||||
+ (NSString *)decodeOIDWithBytes:(unsigned char *)bytes length:(NSUInteger)length {
|
||||
NSMutableArray *objectId = [NSMutableArray array];
|
||||
BOOL inVariableLengthByte = NO;
|
||||
NSUInteger variableLength = 0;
|
||||
for (NSUInteger i = 0; i < length; ++i) {
|
||||
if (i == 0) {
|
||||
// The first byte is actually two values, the top 4 bits are the first value * 40
|
||||
// and the bottom 4 bits are the second value.
|
||||
[objectId addObject:@((NSUInteger)bytes[i] / 40)];
|
||||
[objectId addObject:@((NSUInteger)bytes[i] % 40)];
|
||||
} else {
|
||||
// The remaining bytes are encoded with Variable Length Quantity.
|
||||
unsigned char byte = bytes[i];
|
||||
if (byte & 0x80) {
|
||||
inVariableLengthByte = YES;
|
||||
|
||||
NSUInteger a = (NSUInteger)(byte & ~0x80);
|
||||
variableLength = variableLength << 7;
|
||||
variableLength += a;
|
||||
} else if (inVariableLengthByte) {
|
||||
NSUInteger a = (NSUInteger)(byte & ~0x80);
|
||||
variableLength = variableLength << 7;
|
||||
variableLength += a;
|
||||
inVariableLengthByte = NO;
|
||||
[objectId addObject:@(variableLength)];
|
||||
variableLength = 0;
|
||||
} else {
|
||||
[objectId addObject:@((NSUInteger)byte)];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [objectId componentsJoinedByString:@"."];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,10 +12,9 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTCommonEnums.h"
|
||||
#import "SNTDatabaseTable.h"
|
||||
|
||||
#include "SNTCommonEnums.h"
|
||||
|
||||
@class SNTRule;
|
||||
@class SNTNotificationMessage;
|
||||
|
||||
@@ -55,8 +54,9 @@
|
||||
///
|
||||
/// @param rules Array of SNTRule's to add.
|
||||
/// @param cleanSlate If true, remove all rules before adding the new rules.
|
||||
/// @return YES if all rules were added successfully.
|
||||
/// @param error When returning NO, will be filled with appropriate error.
|
||||
/// @return YES if adding all rules passed, NO if any were rejected.
|
||||
///
|
||||
- (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate;
|
||||
- (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate error:(NSError **)error;
|
||||
|
||||
@end
|
||||
@@ -28,9 +28,8 @@
|
||||
@implementation SNTRuleTable
|
||||
|
||||
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
|
||||
// Save hashes of the signing certs for launchd and santad
|
||||
self.santadCertSHA = [[[[MOLCodesignChecker alloc] initWithSelf] leafCertificate] SHA256];
|
||||
self.launchdCertSHA = [[[[MOLCodesignChecker alloc] initWithPID:1] leafCertificate] SHA256];
|
||||
// Lock this database from other processes
|
||||
[[db executeQuery:@"PRAGMA locking_mode = EXCLUSIVE;"] close];
|
||||
|
||||
uint32_t newVersion = 0;
|
||||
|
||||
@@ -41,23 +40,32 @@
|
||||
@"'type' INTEGER NOT NULL, "
|
||||
@"'custommsg' TEXT"
|
||||
@")"];
|
||||
|
||||
[db executeUpdate:@"CREATE VIEW binrules AS SELECT * FROM rules WHERE type=1"];
|
||||
[db executeUpdate:@"CREATE VIEW certrules AS SELECT * FROM rules WHERE type=2"];
|
||||
|
||||
[db executeUpdate:@"CREATE UNIQUE INDEX rulesunique ON rules (shasum, type)"];
|
||||
|
||||
// Insert the codesigning certs for the running santad and launchd into the initial database.
|
||||
// This helps prevent accidentally denying critical system components while the database
|
||||
// is empty. This 'initial database' will then be cleared on the first successful sync.
|
||||
[db executeUpdate:@"INSERT INTO rules (shasum, state, type) VALUES (?, ?, ?)",
|
||||
self.santadCertSHA, @(RULESTATE_WHITELIST), @(RULETYPE_CERT)];
|
||||
[db executeUpdate:@"INSERT INTO rules (shasum, state, type) VALUES (?, ?, ?)",
|
||||
self.launchdCertSHA, @(RULESTATE_WHITELIST), @(RULETYPE_CERT)];
|
||||
[[SNTConfigurator configurator] setSyncCleanRequired:YES];
|
||||
|
||||
newVersion = 1;
|
||||
}
|
||||
|
||||
[[SNTConfigurator configurator] setSyncCleanRequired:YES];
|
||||
// Save hashes of the signing certs for launchd and santad.
|
||||
// Used to ensure rules for them are not removed.
|
||||
self.santadCertSHA = [[[[MOLCodesignChecker alloc] initWithSelf] leafCertificate] SHA256];
|
||||
self.launchdCertSHA = [[[[MOLCodesignChecker alloc] initWithPID:1] leafCertificate] SHA256];
|
||||
|
||||
// Ensure the certificates used to sign the running launchd/santad are whitelisted.
|
||||
// If they weren't previously and the database is not new, log an error.
|
||||
int ruleCount = [db intForQuery:@"SELECT COUNT(*)"
|
||||
@"FROM certrules "
|
||||
@"WHERE (shasum=? OR shasum=?) AND state=?",
|
||||
self.santadCertSHA, self.launchdCertSHA, @(SNTRuleStateWhitelist)];
|
||||
if (ruleCount != 2) {
|
||||
if (version > 0) LOGE(@"Started without launchd/santad certificate rules in place!");
|
||||
[db executeUpdate:@"INSERT INTO rules (shasum, state, type) VALUES (?, ?, ?)",
|
||||
self.santadCertSHA, @(SNTRuleStateWhitelist), @(SNTRuleTypeCertificate)];
|
||||
[db executeUpdate:@"INSERT INTO rules (shasum, state, type) VALUES (?, ?, ?)",
|
||||
self.launchdCertSHA, @(SNTRuleStateWhitelist), @(SNTRuleTypeCertificate)];
|
||||
}
|
||||
|
||||
return newVersion;
|
||||
@@ -130,9 +138,9 @@
|
||||
|
||||
#pragma mark Adding
|
||||
|
||||
- (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate {
|
||||
- (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate error:(NSError **)error {
|
||||
if (!rules || rules.count < 1) {
|
||||
LOGE(@"Received request to add rules with nil/empty array.");
|
||||
[self fillError:error code:SNTRuleTableErrorEmptyRuleArray message:nil];
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -142,13 +150,14 @@
|
||||
// Protect rules for santad/launchd certificates.
|
||||
NSPredicate *p = [NSPredicate predicateWithFormat:
|
||||
@"(SELF.shasum = %@ OR SELF.shasum = %@) AND SELF.type = %d",
|
||||
self.santadCertSHA, self.launchdCertSHA, RULETYPE_CERT];
|
||||
self.santadCertSHA, self.launchdCertSHA, SNTRuleTypeCertificate];
|
||||
NSArray *requiredHashes = [rules filteredArrayUsingPredicate:p];
|
||||
p = [NSPredicate predicateWithFormat:@"SELF.state == %d", RULESTATE_WHITELIST];
|
||||
p = [NSPredicate predicateWithFormat:@"SELF.state == %d", SNTRuleStateWhitelist];
|
||||
NSArray *requiredHashesWhitelist = [requiredHashes filteredArrayUsingPredicate:p];
|
||||
if ((cleanSlate && requiredHashesWhitelist.count != 2) ||
|
||||
(requiredHashes.count != requiredHashesWhitelist.count)) {
|
||||
LOGE(@"Received request to remove whitelist for launchd/santad ceritifcates.");
|
||||
LOGE(@"Received request to remove whitelist for launchd/santad certificates.");
|
||||
[self fillError:error code:SNTRuleTableErrorMissingRequiredRule message:nil];
|
||||
*rollback = failed = YES;
|
||||
return;
|
||||
}
|
||||
@@ -159,14 +168,18 @@
|
||||
|
||||
for (SNTRule *rule in rules) {
|
||||
if (![rule isKindOfClass:[SNTRule class]] || rule.shasum.length == 0 ||
|
||||
rule.state == RULESTATE_UNKNOWN || rule.type == RULETYPE_UNKNOWN) {
|
||||
rule.state == SNTRuleStateUnknown || rule.type == SNTRuleTypeUnknown) {
|
||||
[self fillError:error code:SNTRuleTableErrorInvalidRule message:nil];
|
||||
*rollback = failed = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
if (rule.state == RULESTATE_REMOVE) {
|
||||
if (rule.state == SNTRuleStateRemove) {
|
||||
if (![db executeUpdate:@"DELETE FROM rules WHERE shasum=? AND type=?",
|
||||
rule.shasum, @(rule.type)]) {
|
||||
[self fillError:error
|
||||
code:SNTRuleTableErrorRemoveFailed
|
||||
message:[db lastErrorMessage]];
|
||||
*rollback = failed = YES;
|
||||
return;
|
||||
}
|
||||
@@ -175,6 +188,9 @@
|
||||
@"(shasum, state, type, custommsg) "
|
||||
@"VALUES (?, ?, ?, ?);",
|
||||
rule.shasum, @(rule.state), @(rule.type), rule.customMsg]) {
|
||||
[self fillError:error
|
||||
code:SNTRuleTableErrorInsertOrReplaceFailed
|
||||
message:[db lastErrorMessage]];
|
||||
*rollback = failed = YES;
|
||||
return;
|
||||
}
|
||||
@@ -185,4 +201,34 @@
|
||||
return !failed;
|
||||
}
|
||||
|
||||
// Helper to create an NSError where necessary.
|
||||
// The return value is irrelevant but the static analyzer complains if it's not a BOOL.
|
||||
- (BOOL)fillError:(NSError **)error code:(SNTRuleTableError)code message:(NSString *)message {
|
||||
if (!error) return NO;
|
||||
|
||||
NSMutableDictionary *d = [NSMutableDictionary dictionary];
|
||||
switch (code) {
|
||||
case SNTRuleTableErrorEmptyRuleArray:
|
||||
d[NSLocalizedDescriptionKey] = @"Empty rule array";
|
||||
break;
|
||||
case SNTRuleTableErrorInvalidRule:
|
||||
d[NSLocalizedDescriptionKey] = @"Rule array contained invalid entry";
|
||||
break;
|
||||
case SNTRuleTableErrorInsertOrReplaceFailed:
|
||||
d[NSLocalizedDescriptionKey] = @"A database error occurred while inserting/replacing a rule";
|
||||
break;
|
||||
case SNTRuleTableErrorRemoveFailed:
|
||||
d[NSLocalizedDescriptionKey] = @"A database error occurred while deleting a rule";
|
||||
break;
|
||||
case SNTRuleTableErrorMissingRequiredRule:
|
||||
d[NSLocalizedDescriptionKey] = @"A required rule was requested to be deleted";
|
||||
break;
|
||||
}
|
||||
|
||||
if (message) d[NSLocalizedFailureReasonErrorKey] = message;
|
||||
|
||||
*error = [NSError errorWithDomain:@"com.google.santad.ruletable" code:code userInfo:d];
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -14,11 +14,11 @@
|
||||
|
||||
#import "SNTApplication.h"
|
||||
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "SNTCommonEnums.h"
|
||||
|
||||
#import "SNTCommonEnums.h"
|
||||
#import "SNTConfigurator.h"
|
||||
#import "SNTDaemonControlController.h"
|
||||
#import "SNTDatabaseController.h"
|
||||
@@ -34,6 +34,7 @@
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@interface SNTApplication ()
|
||||
@property DASessionRef diskArbSession;
|
||||
@property SNTDriverManager *driverManager;
|
||||
@property SNTEventLog *eventLog;
|
||||
@property SNTExecutionController *execController;
|
||||
@@ -51,8 +52,6 @@
|
||||
|
||||
if (!_driverManager) {
|
||||
LOGE(@"Failed to connect to driver, exiting.");
|
||||
|
||||
// TODO(rah): Consider trying to load the extension from within santad.
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -81,12 +80,34 @@
|
||||
_controlConnection.exportedObject = dc;
|
||||
[_controlConnection resume];
|
||||
|
||||
_configFileWatcher = [[SNTFileWatcher alloc] initWithFilePath:kDefaultConfigFilePath handler:^{
|
||||
[[SNTConfigurator configurator] reloadConfigData];
|
||||
__block SNTClientMode origMode = [[SNTConfigurator configurator] clientMode];
|
||||
_configFileWatcher = [[SNTFileWatcher alloc] initWithFilePath:kDefaultConfigFilePath
|
||||
handler:^(unsigned long data) {
|
||||
if (data & DISPATCH_VNODE_ATTRIB) {
|
||||
const char *cPath = [kDefaultConfigFilePath fileSystemRepresentation];
|
||||
struct stat fileStat;
|
||||
stat(cPath, &fileStat);
|
||||
int mask = S_IRWXU | S_IRWXG | S_IRWXO;
|
||||
int desired = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
|
||||
if (fileStat.st_uid != 0 || fileStat.st_gid != 0 || (fileStat.st_mode & mask) != desired) {
|
||||
LOGD(@"Config file permissions changed, fixing.");
|
||||
chown(cPath, 0, 0);
|
||||
chmod(cPath, desired);
|
||||
}
|
||||
} else {
|
||||
LOGD(@"Config file changed, reloading.");
|
||||
[[SNTConfigurator configurator] reloadConfigData];
|
||||
|
||||
// Ensure config file remains root:wheel 0644
|
||||
chown([kDefaultConfigFilePath fileSystemRepresentation], 0, 0);
|
||||
chmod([kDefaultConfigFilePath fileSystemRepresentation], 0644);
|
||||
// Flush cache if client just went into lockdown.
|
||||
SNTClientMode newMode = [[SNTConfigurator configurator] clientMode];
|
||||
if (origMode != newMode) {
|
||||
origMode = newMode;
|
||||
if (newMode == SNTClientModeLockdown) {
|
||||
LOGI(@"Changed client mode, flushing cache.");
|
||||
[self.driverManager flushCache];
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
_eventLog = [[SNTEventLog alloc] init];
|
||||
@@ -108,6 +129,7 @@
|
||||
|
||||
[self performSelectorInBackground:@selector(beginListeningForDecisionRequests) withObject:nil];
|
||||
[self performSelectorInBackground:@selector(beginListeningForLogRequests) withObject:nil];
|
||||
[self performSelectorInBackground:@selector(beginListeningForDiskMounts) withObject:nil];
|
||||
}
|
||||
|
||||
- (void)beginListeningForDecisionRequests {
|
||||
@@ -124,7 +146,7 @@
|
||||
}
|
||||
case ACTION_REQUEST_BINARY: {
|
||||
dispatch_async(exec_queue, ^{
|
||||
[self.execController validateBinaryWithMessage:message];
|
||||
[_execController validateBinaryWithMessage:message];
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -141,7 +163,7 @@
|
||||
dispatch_queue_t log_queue = dispatch_queue_create(
|
||||
"com.google.santad.log_queue", DISPATCH_QUEUE_CONCURRENT);
|
||||
dispatch_set_target_queue(
|
||||
log_queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0));
|
||||
log_queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
|
||||
|
||||
[self.driverManager listenForLogRequests:^(santa_message_t message) {
|
||||
@autoreleasepool {
|
||||
@@ -155,14 +177,14 @@
|
||||
NSRegularExpression *re = [[SNTConfigurator configurator] fileChangesRegex];
|
||||
NSString *path = @(message.path);
|
||||
if ([re numberOfMatchesInString:path options:0 range:NSMakeRange(0, path.length)]) {
|
||||
[self.eventLog logFileModification:message];
|
||||
[_eventLog logFileModification:message];
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ACTION_NOTIFY_EXEC: {
|
||||
dispatch_async(log_queue, ^{
|
||||
[self.eventLog logAllowedExecution:message];
|
||||
[_eventLog logAllowedExecution:message];
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -174,4 +196,38 @@
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)beginListeningForDiskMounts {
|
||||
dispatch_queue_t disk_queue = dispatch_queue_create(
|
||||
"com.google.santad.disk_queue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_diskArbSession = DASessionCreate(NULL);
|
||||
DASessionSetDispatchQueue(_diskArbSession, disk_queue);
|
||||
|
||||
DARegisterDiskAppearedCallback(
|
||||
_diskArbSession, NULL, diskAppearedCallback, (__bridge void *)self);
|
||||
DARegisterDiskDescriptionChangedCallback(
|
||||
_diskArbSession, NULL, NULL, diskDescriptionChangedCallback, (__bridge void *)self);
|
||||
DARegisterDiskDisappearedCallback(
|
||||
_diskArbSession, NULL, diskDisappearedCallback, (__bridge void *)self);
|
||||
}
|
||||
|
||||
void diskAppearedCallback(DADiskRef disk, void *context) {
|
||||
SNTApplication *app = (__bridge SNTApplication *)context;
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
[app.eventLog logDiskAppeared:props];
|
||||
}
|
||||
|
||||
void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
|
||||
SNTApplication *app = (__bridge SNTApplication *)context;
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (props[@"DAVolumePath"]) [app.eventLog logDiskAppeared:props];
|
||||
}
|
||||
|
||||
void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
SNTApplication *app = (__bridge SNTApplication *)context;
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
[app.eventLog logDiskDisappeared:props];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "SNTCommonEnums.h"
|
||||
#import "SNTCommonEnums.h"
|
||||
|
||||
///
|
||||
/// Store information about executions from decision making for later logging.
|
||||
@@ -20,7 +20,7 @@
|
||||
@interface SNTCachedDecision : NSObject
|
||||
|
||||
@property uint64_t vnodeId;
|
||||
@property santa_eventstate_t decision;
|
||||
@property SNTEventState decision;
|
||||
@property NSString *decisionExtra;
|
||||
@property NSString *sha256;
|
||||
@property NSString *certSHA256;
|
||||
|
||||
@@ -33,6 +33,7 @@ double watchdogCPUPeak = 0;
|
||||
double watchdogRAMPeak = 0;
|
||||
|
||||
@interface SNTDaemonControlController ()
|
||||
@property NSString *_syncXsrfToken;
|
||||
@property dispatch_source_t syncTimer;
|
||||
@end
|
||||
|
||||
@@ -90,6 +91,10 @@ double watchdogRAMPeak = 0;
|
||||
reply([self.driverManager flushCache]);
|
||||
}
|
||||
|
||||
- (void)checkCacheForVnodeID:(uint64_t)vnodeID withReply:(void (^)(santa_action_t))reply {
|
||||
reply([self.driverManager checkCache:vnodeID]);
|
||||
}
|
||||
|
||||
#pragma mark Database ops
|
||||
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate))reply {
|
||||
@@ -97,23 +102,20 @@ double watchdogRAMPeak = 0;
|
||||
reply([rdb binaryRuleCount], [rdb certificateRuleCount]);
|
||||
}
|
||||
|
||||
- (void)databaseRuleAddRule:(SNTRule *)rule cleanSlate:(BOOL)cleanSlate
|
||||
reply:(void (^)(BOOL success))reply {
|
||||
[self databaseRuleAddRules:@[ rule ] cleanSlate:cleanSlate reply:reply];
|
||||
}
|
||||
|
||||
- (void)databaseRuleAddRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate
|
||||
reply:(void (^)(BOOL success))reply {
|
||||
BOOL success = [[SNTDatabaseController ruleTable] addRules:rules cleanSlate:cleanSlate];
|
||||
- (void)databaseRuleAddRules:(NSArray *)rules
|
||||
cleanSlate:(BOOL)cleanSlate
|
||||
reply:(void (^)(NSError *error))reply {
|
||||
NSError *error;
|
||||
[[SNTDatabaseController ruleTable] addRules:rules cleanSlate:cleanSlate error:&error];
|
||||
|
||||
// If any rules were added that were not whitelist, flush cache.
|
||||
NSPredicate *p = [NSPredicate predicateWithFormat:@"SELF.state != %d", RULESTATE_WHITELIST];
|
||||
NSPredicate *p = [NSPredicate predicateWithFormat:@"SELF.state != %d", SNTRuleStateWhitelist];
|
||||
if ([rules filteredArrayUsingPredicate:p].count || cleanSlate) {
|
||||
LOGI(@"Received non-whitelist rule, flushing cache");
|
||||
[self.driverManager flushCache];
|
||||
}
|
||||
|
||||
reply(success);
|
||||
reply(error);
|
||||
}
|
||||
|
||||
- (void)databaseEventCount:(void (^)(int64_t count))reply {
|
||||
@@ -132,14 +134,34 @@ double watchdogRAMPeak = 0;
|
||||
[[SNTDatabaseController eventTable] deleteEventsWithIds:ids];
|
||||
}
|
||||
|
||||
- (void)databaseBinaryRuleForSHA256:(NSString *)sha256 reply:(void (^)(SNTRule *))reply {
|
||||
reply([[SNTDatabaseController ruleTable] binaryRuleForSHA256:sha256]);
|
||||
}
|
||||
|
||||
- (void)databaseCertificateRuleForSHA256:(NSString *)sha256 reply:(void (^)(SNTRule *))reply {
|
||||
reply([[SNTDatabaseController ruleTable] certificateRuleForSHA256:sha256]);
|
||||
}
|
||||
|
||||
#pragma mark Config Ops
|
||||
|
||||
- (void)clientMode:(void (^)(santa_clientmode_t))reply {
|
||||
- (void)clientMode:(void (^)(SNTClientMode))reply {
|
||||
reply([[SNTConfigurator configurator] clientMode]);
|
||||
}
|
||||
|
||||
- (void)setClientMode:(santa_clientmode_t)mode reply:(void (^)())reply {
|
||||
[[SNTConfigurator configurator] setClientMode:mode];
|
||||
- (void)setClientMode:(SNTClientMode)mode reply:(void (^)())reply {
|
||||
if ([[SNTConfigurator configurator] clientMode] != mode) {
|
||||
[[SNTConfigurator configurator] setClientMode:mode];
|
||||
[[self.notQueue.notifierConnection remoteObjectProxy] postClientModeNotification:mode];
|
||||
}
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)xsrfToken:(void (^)(NSString *))reply {
|
||||
reply(self._syncXsrfToken);
|
||||
}
|
||||
|
||||
- (void)setXsrfToken:(NSString *)token reply:(void (^)())reply {
|
||||
self._syncXsrfToken = token;
|
||||
reply();
|
||||
}
|
||||
|
||||
|
||||
@@ -55,4 +55,9 @@
|
||||
///
|
||||
- (BOOL)flushCache;
|
||||
|
||||
///
|
||||
/// Check the kernel cache for a VnodeID
|
||||
///
|
||||
-(santa_action_t)checkCache:(uint64_t)vnodeID;
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
|
||||
#import "SNTDriverManager.h"
|
||||
|
||||
#include <IOKit/IODataQueueClient.h>
|
||||
#include <mach/mach.h>
|
||||
#import <IOKit/IODataQueueClient.h>
|
||||
#import <IOKit/Kext/KextManager.h>
|
||||
|
||||
#include "SNTLogging.h"
|
||||
#import "SNTLogging.h"
|
||||
|
||||
@interface SNTDriverManager ()
|
||||
@property io_connect_t connection;
|
||||
@@ -41,6 +41,9 @@ static const int MAX_DELAY = 15;
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Attempt to load driver. It may already be running, so ignore any return value.
|
||||
KextManagerLoadKextWithIdentifier(CFSTR(USERCLIENT_ID), NULL);
|
||||
|
||||
// Locate driver. Wait for it if necessary.
|
||||
int delay = 1;
|
||||
do {
|
||||
@@ -95,13 +98,9 @@ static const int MAX_DELAY = 15;
|
||||
withCallback:(void (^)(santa_message_t))callback {
|
||||
kern_return_t kr;
|
||||
|
||||
mach_port_t receivePort = 0;
|
||||
IODataQueueMemory *queueMemory = NULL;
|
||||
mach_vm_address_t address = 0;
|
||||
mach_vm_size_t size = 0;
|
||||
|
||||
// Allocate a mach port to receive notifactions from the IODataQueue
|
||||
if (!(receivePort = IODataQueueAllocateNotificationPort())) {
|
||||
mach_port_t receivePort = IODataQueueAllocateNotificationPort();
|
||||
if (receivePort == MACH_PORT_NULL) {
|
||||
LOGD(@"Failed to allocate notification port");
|
||||
return;
|
||||
}
|
||||
@@ -115,15 +114,16 @@ static const int MAX_DELAY = 15;
|
||||
}
|
||||
|
||||
// This will call clientMemoryForType() inside our user client class.
|
||||
kr = IOConnectMapMemory(self.connection, type, mach_task_self(),
|
||||
&address, &size, kIOMapAnywhere);
|
||||
mach_vm_address_t address = 0;
|
||||
mach_vm_size_t size = 0;
|
||||
kr = IOConnectMapMemory(self.connection, type, mach_task_self(), &address, &size, kIOMapAnywhere);
|
||||
if (kr != kIOReturnSuccess) {
|
||||
LOGD(@"Failed to map memory for type %d: %d", type, kr);
|
||||
mach_port_destroy(mach_task_self(), receivePort);
|
||||
return;
|
||||
}
|
||||
|
||||
queueMemory = (IODataQueueMemory *)address;
|
||||
IODataQueueMemory *queueMemory = (IODataQueueMemory *)address;
|
||||
|
||||
do {
|
||||
while (IODataQueueDataAvailable(queueMemory)) {
|
||||
@@ -148,14 +148,14 @@ static const int MAX_DELAY = 15;
|
||||
- (kern_return_t)postToKernelAction:(santa_action_t)action forVnodeID:(uint64_t)vnodeId {
|
||||
switch (action) {
|
||||
case ACTION_RESPOND_ALLOW:
|
||||
return IOConnectCallScalarMethod(self.connection,
|
||||
return IOConnectCallScalarMethod(_connection,
|
||||
kSantaUserClientAllowBinary,
|
||||
&vnodeId,
|
||||
1,
|
||||
0,
|
||||
0);
|
||||
case ACTION_RESPOND_DENY:
|
||||
return IOConnectCallScalarMethod(self.connection,
|
||||
return IOConnectCallScalarMethod(_connection,
|
||||
kSantaUserClientDenyBinary,
|
||||
&vnodeId,
|
||||
1,
|
||||
@@ -170,7 +170,7 @@ static const int MAX_DELAY = 15;
|
||||
uint32_t input_count = 1;
|
||||
uint64_t cache_count = 0;
|
||||
|
||||
IOConnectCallScalarMethod(self.connection,
|
||||
IOConnectCallScalarMethod(_connection,
|
||||
kSantaUserClientCacheCount,
|
||||
0,
|
||||
0,
|
||||
@@ -180,8 +180,21 @@ static const int MAX_DELAY = 15;
|
||||
}
|
||||
|
||||
- (BOOL)flushCache {
|
||||
return IOConnectCallScalarMethod(self.connection,
|
||||
return IOConnectCallScalarMethod(_connection,
|
||||
kSantaUserClientClearCache, 0, 0, 0, 0) == KERN_SUCCESS;
|
||||
}
|
||||
|
||||
- (santa_action_t)checkCache:(uint64_t)vnodeID {
|
||||
uint32_t input_count = 1;
|
||||
uint64_t vnode_action = 0;
|
||||
|
||||
IOConnectCallScalarMethod(_connection,
|
||||
kSantaUserClientCheckCache,
|
||||
&vnodeID,
|
||||
1,
|
||||
&vnode_action,
|
||||
&input_count);
|
||||
return (santa_action_t)vnode_action;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user