Compare commits

...

44 Commits

Author SHA1 Message Date
Russell Hancox
d82e64aa5f Project: Split integration VM license into its own LICENSE file (#1147) 2023-08-08 13:29:06 -04:00
Ivan Tadeu Ferreira Antunes Filho
a9c1c730be Project: Cast some enums to int (#1146)
Allows the string displaying the enum to format it using %d.

Fixes the error: `error: format specifies type 'int' but the argument has type 'T' [-Werror,-Wformat]`
2023-08-08 13:08:11 -04:00
Matt W
6c4362d8bb Add hot cache for targets of read only policies (#1145)
* Add hot cache for file reads

* Clear cache on policy change

* Prevent unbounded cache growth

* Move cache impl to its own class

* Add some additional tests

* Cleanup

* Comment cleanup

* Switch to absl containers

* Use default absl::Hash instead of custom hasher

* Removing another reference to PairHash

* Remove unused imports
2023-08-08 12:38:33 -04:00
Russell Hancox
c1189493e8 sync/UI: Add ability to send custom URLs for blocking rules. (#1140)
This allows a sync server to send a `custom_url` field along with a rule blocking execution and this will be used as the URL for the "open" button in place of the normally generated URL.
2023-08-04 15:01:45 -04:00
Russell Hancox
aaa0d40841 sync: Add SyncExtraHeaders config option. (#1144)
* sync: Add SyncExtraHeaders config option.

Fixes #1143
2023-08-03 23:16:59 -04:00
Matt W
a424c4afca Only update daemon settings when sync settings explicitly set (#1142) 2023-08-03 16:18:40 -04:00
Matt W
2847397b66 Have distributed notifications delivered immediately (#1141) 2023-08-03 15:49:10 -04:00
Matt W
ad8b4b6646 Check if spool dir has changed before estimating size (#1138) 2023-08-03 14:54:14 -04:00
alexgraehl
39ee9e7d48 sync: Change backoff in SNTSyncStage.m to be exponential (#1135)
* Change backoff in SNTSyncStage.m to be exponential instead of linear
* Improves the log message to indicate that the job will ALWAYS abort after N retries. Previously, it was not clear why it would try 5 times and then give up.
2023-07-26 15:55:37 -04:00
Matt W
3cccacc3fb Add additional dep to satisfy import issue (#1134) 2023-07-26 12:50:29 -04:00
Matt W
6ed5bcd808 Enforce expected case for various rule type identifiers (#1132)
* Bump DB version. Ensure proper casing for rule identifiers on insert.

* Minor comment fixes, more test cases

* Handle SigningIDs using the delimiter character

* lint

* PR feedback
2023-07-26 12:31:28 -04:00
Matt W
bcac65a23e Wire up TTYWriter instance to the file access client (#1129) 2023-07-26 00:11:36 -04:00
Matt W
03fcd0c906 Add more file access config options (#1128)
* New file access config options supporting silencing and custom messages

* Rename custom message key
2023-07-26 00:01:04 -04:00
p-harrison
d3b71a3ba8 Update sync-protocol.md to include SIGNINGID rule type (#1130)
* Update sync-protocol.md

Couple of formatting changes, added SIGNINGID as a rule type

* Update docs/development/sync-protocol.md

Co-authored-by: Matt W <436037+mlw@users.noreply.github.com>

---------

Co-authored-by: Matt W <436037+mlw@users.noreply.github.com>
2023-07-25 14:31:13 -04:00
Pete Markowsky
9e124f4c51 Add kSyncEnableCleanSyncEventUpload to the _forcedConfigKeyTypes dict (#1123)
* Add kSyncEnableCleanSyncEventUpload to the _forcedConfigTypes dict.

* Add KVO helper.

---------

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
2023-07-06 17:39:51 -04:00
Matt W
cd719ccef4 Fix issue with invalid lengths (#1122)
* Fix issue with invalid lengths

* Disable clang format around a small block of code for now
2023-07-06 11:22:18 -04:00
Matt W
dde42ee686 Fix check to detect changes to StaticRules (#1121) 2023-06-30 16:39:52 -04:00
Pete Markowsky
d144e27798 Fix rule evaluation for TeamID and SigningID rules when encountering broken signatures. (#1120) 2023-06-30 09:54:27 -04:00
Matt W
afc2c216b8 Add include for proto status stub (#1119) 2023-06-29 13:32:14 -04:00
Matt W
03d7556f22 Use angle brackets for includes (#1118) 2023-06-29 11:55:46 -04:00
Nick Gregory
020827b091 Fix memleak in fsspool (#1115) 2023-06-29 10:17:08 -04:00
Russell Hancox
baa31a5db0 Conf: Update notarization_tool in signing script (#1116) 2023-06-28 22:32:58 -04:00
Pete Markowsky
9ba7075596 Add macOS 13 to the test matrix. (#1113) 2023-06-27 13:22:36 -04:00
Pete Markowsky
5d08538639 Add Support for Logging to JSON (beta feature) (#1112)
* Add support for logging protobuf to JSON.

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
2023-06-23 10:06:45 -04:00
Matt W
e73bafb596 Fix build issues due to macOS 13.3 SDK changes (#1110)
* Fix minor build issues due to changes in the macOS 13.3 SDK

* Disable -Wunknown-warning-option
2023-06-20 22:23:55 -04:00
Matt W
1e92d109a7 Basic dialog functionality when access to a watch item is denied (#1106)
* Basic working prototype to display a UI on blocked file access

* Force watch items policies to be silent for now

* Remove unused view

* Refactor to not use newer SwiftUI features

* Address PR feedback
2023-06-19 14:00:35 -04:00
Matt W
6a6aa6dce8 Abstract TTY writing so multiple writers can be synchronized (#1108)
* Abstract TTY writing so multiple writers can be synchronized

* Address PR feedback
2023-06-13 20:19:50 -04:00
Matt W
0715033d6a Migrate to new SNTRuleType enum values (#1107)
* Migrate to new SNTRuleType enum values

* Bump table version. Fix comments to address PR feedback.

* Add log message when a downgrade detected
2023-06-09 11:50:42 -04:00
Matt W
123d7a2d6a Update docs for signing id rules (#1105)
* Update docs for signing id rules

* Formatting, Address PR feedback
2023-05-30 13:27:29 -04:00
Matt W
7b4d997589 Fix missing check for FileChangesRegex (#1102) 2023-05-22 16:13:06 -04:00
Matt W
5307bd9b7f Fix precedence for static rule evaluation, update santactl fileinfo output. (#1100) 2023-05-18 15:05:23 -04:00
Matt W
0622e6de71 Handle database downgrade scenarios gracefully (#1099) 2023-05-17 04:31:40 +02:00
Russell Hancox
e7c32ae87d Update SECURITY.md (#1098) 2023-05-12 10:30:58 -04:00
Matt W
deaf3a638c Add new rule type for Signing IDs (#1090)
* WIP: Signing ID rules

* WIP: More work supporting signing ID rules

* Expanded exec controller tests for signing ID and team ID

* wip all current tests now pass

* Added integration tests

* Branch cleanup

* Update protobuf tests for signing id reason types

* Remove old commented out code

---------

Co-authored-by: Russell Hancox <russell@hancox.us>
2023-05-12 09:22:46 -04:00
Matt W
8a7f1142a8 Stop unmuting the default mute set unnecessarily. (#1095)
* Stop unmuting the default mute set unnecessarily.

* lint

* Added note to docs explaining operations from default mute set binaries aren't logged
2023-05-10 09:07:13 -04:00
Matt W
c180205059 Return unique_ptr from Enrich instead of shared_ptr (#1093) 2023-05-08 10:55:38 -04:00
Matt W
337df0aa31 Don't establish the FAA client pre-macOS 13 (#1091)
* Don't establish the FAA client pre-macOS 13

* Only watch FAA keys on macOS 13 and newer
2023-05-05 15:33:34 -04:00
Russell Hancox
e2b099aa50 santactl/rule: Fix --path argument (#1089)
Fixes #1088
2023-05-04 17:57:59 -04:00
Pete Markowsky
fc4e29f34c Docs: Added instructions for how to use config-overrides.plist (#1077)
* Added instructions for how to use config-overrides

---------

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
2023-05-01 16:16:11 -04:00
Matt W
bf3b6bc6e2 Inject additional dependencies into the serializers (#1078)
* Injects dependecies for decision cache and client mode lookup

* Fix up tests

* Stored client mode at decision time. Remove clientMode func injection.

* PR Feedback, group property members
2023-05-01 15:13:54 -04:00
Matt W
b810fc81e1 Add support to file monitoring config to invert process exceptions (#1083)
* Add support to file monitoring config to invert process exceptions

* Update docs

* Added link to github issue
2023-05-01 15:04:40 -04:00
Matt W
3b3aa999c5 Switch SNTEventState to uint64_t, reposition flag values and masks (#1086) 2023-05-01 14:37:11 -04:00
Faizan
59428f3be3 docs: Fix documentation for clean sync field in the preflight request. (#1082)
The 'request_clean_sync' field is set here: https://github.com/google/santa/blob/main/Source/santasyncservice/SNTSyncPreflight.m#L76
The constant is defined here: https://github.com/google/santa/blob/main/Source/common/SNTSyncConstants.m#L27
2023-04-27 23:38:44 -04:00
Jason McCandless
ae6451a9b2 docs: Clarify that execution_time, file_bundle_hash_millis and quarantine_timestamp are float64 (#1080) 2023-04-27 18:54:02 -04:00
124 changed files with 3104 additions and 707 deletions

View File

@@ -3,6 +3,10 @@ build --apple_generate_dsym --define=apple.propagate_embedded_extra_outputs=yes
build --copt=-Werror
build --copt=-Wall
build --copt=-Wno-error=deprecated-declarations
# Disable -Wunknown-warning-option because deprecated-non-prototype
# isn't recognized on older SDKs
build --copt=-Wno-unknown-warning-option
build --copt=-Wno-error=deprecated-non-prototype
build --per_file_copt=.*\.mm\$@-std=c++17
build --cxxopt=-std=c++17

View File

@@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-11, macos-12]
os: [macos-11, macos-12, macos-13]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
@@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-11, macos-12]
os: [macos-11, macos-12, macos-13]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3

View File

@@ -2,5 +2,5 @@
# Example NOTARIZATION_TOOL wrapper.
/usr/bin/xcrun altool --notarize-app "${2}" --primary-bundle-id "${4}" \
-u "${NOTARIZATION_USERNAME}" -p "${NOTARIZATION_PASSWORD}"
/usr/bin/xcrun notarytool submit "${2}" --wait \
--apple-id "${NOTARIZATION_USERNAME}" --password "${NOTARIZATION_PASSWORD}"

View File

@@ -28,8 +28,6 @@
# tool around the tool to use for notarization. The tool must take 2 flags:
# --file
# - pointing at a zip file containing the artifact to notarize
# --primary-bundle-id
# - specifying the CFBundleID of the artifact being notarized
[[ -n "${NOTARIZATION_TOOL}" ]] || die "NOTARIZATION_TOOL unset"
# ARTIFACTS_DIR is a required environment variable pointing at a directory to
@@ -92,7 +90,7 @@ for ARTIFACT in "${INPUT_SYSX}" "${INPUT_APP}"; do
echo "notarizing ${BN}"
PBID=$(/usr/bin/defaults read "${ARTIFACT}/Contents/Info.plist" CFBundleIdentifier)
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${BN}.zip" --primary-bundle-id "${PBID}"
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${BN}.zip"
done
# Staple the App.
@@ -166,8 +164,7 @@ echo "verifying pkg signature"
/usr/sbin/pkgutil --check-signature "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" || die "bad pkg signature"
echo "notarizing pkg"
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" \
--primary-bundle-id "com.google.santa"
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg"
echo "stapling pkg"
/usr/bin/xcrun stapler staple "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" || die "failed to staple pkg"
@@ -179,7 +176,7 @@ echo "wrapping pkg in dmg"
-srcfolder "${SCRATCH}/${RELEASE_NAME}/" "${DMG_PATH}" || die "failed to wrap pkg in dmg"
echo "notarizing dmg"
"${NOTARIZATION_TOOL}" --file "${DMG_PATH}" --primary-bundle-id "com.google.santa"
"${NOTARIZATION_TOOL}" --file "${DMG_PATH}"
echo "stapling dmg"
/usr/bin/xcrun stapler staple "${DMG_PATH}" || die "failed to staple dmg"

View File

@@ -201,12 +201,3 @@
See the License for the specific language governing permissions and
limitations under the License.
------------------
Files: Testing/integration/VM/*
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,12 +1,14 @@
# Reporting a Vulnerability
If you believe you have found a security vulnerability, we would appreciate private disclosure
so that we can work on a fix before disclosure. Any vulnerabilities reported to us will be
If you believe you have found a security vulnerability, we would appreciate a private report
so that we can work on and release a fix before public disclosure. Any vulnerabilities reported to us will be
disclosed publicly either when a new version with fixes is released or 90 days has passed,
whichever comes first.
To report vulnerabilities to us privately, please e-mail `santa-team@google.com`.
If you want to encrypt your e-mail, you can use our GPG key `0x92AFE41DAB49BBB6`
available on keyserver.ubuntu.com:
To report vulnerabilities to us privately, either:
`gpg --keyserver keyserver.ubuntu.com --recv-key 0x92AFE41DAB49BBB6`
1) Report the vulnerability [through GitHub](https://github.com/google/santa/security/advisories/new).
2) E-mail `santa-team@google.com`. If you want to encrypt your e-mail, you can use our GPG key `0x92AFE41DAB49BBB6` available on keyserver.ubuntu.com:
`gpg --keyserver keyserver.ubuntu.com --recv-key 0x92AFE41DAB49BBB6`

View File

@@ -133,6 +133,19 @@ objc_library(
],
)
objc_library(
name = "SNTFileAccessEvent",
srcs = ["SNTFileAccessEvent.m"],
hdrs = ["SNTFileAccessEvent.h"],
module_name = "santa_common_SNTFileAccessEvent",
sdk_frameworks = [
"Foundation",
],
deps = [
"@MOLCertificate",
],
)
objc_library(
name = "SNTCommonEnums",
textual_hdrs = ["SNTCommonEnums.h"],
@@ -225,7 +238,10 @@ objc_library(
santa_unit_test(
name = "SNTRuleTest",
srcs = ["SNTRuleTest.m"],
deps = [":SNTRule"],
deps = [
":SNTCommonEnums",
":SNTRule",
],
)
objc_library(

View File

@@ -42,7 +42,7 @@
/// Return a URL generated from the EventDetailURL configuration key
/// after replacing templates in the URL with values from the event.
///
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event;
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url;
///
/// Strip HTML from a string, replacing <br /> with newline.

View File

@@ -109,14 +109,31 @@
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event {
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
// The following "format strings" will be replaced in the URL, if they are present:
//
// %file_identifier% - The SHA-256 of the binary being executed.
// %bundle_or_file_identifier% - The hash of the bundle containing this file or the file itself,
// if no bundle hash is present.
// %username% - The executing user's name.
// %machine_id% - The configured machine ID for this host.
// %hostname% - The machine's FQDN.
// %uuid% - The machine's UUID.
// %serial% - The machine's serial number.
//
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url {
SNTConfigurator *config = [SNTConfigurator configurator];
NSString *hostname = [SNTSystemInfo longHostname];
NSString *uuid = [SNTSystemInfo hardwareUUID];
NSString *serial = [SNTSystemInfo serialNumber];
NSString *formatStr = config.eventDetailURL;
NSString *formatStr = url;
if (!url.length) formatStr = config.eventDetailURL;
if (!formatStr.length) return nil;
if ([formatStr isEqualToString:@"null"]) return nil;
if (event.fileSHA256) {
// This key is deprecated, use %file_identifier% or %bundle_or_file_identifier%
@@ -148,7 +165,9 @@
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%serial%" withString:serial];
}
return [NSURL URLWithString:formatStr];
NSURL *u = [NSURL URLWithString:formatStr];
if (!u) LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
return u;
}
@end

View File

@@ -29,6 +29,7 @@
@property SantaVnode vnodeId;
@property SNTEventState decision;
@property SNTClientMode decisionClientMode;
@property NSString *decisionExtra;
@property NSString *sha256;
@@ -36,10 +37,12 @@
@property NSString *certCommonName;
@property NSArray<MOLCertificate *> *certChain;
@property NSString *teamID;
@property NSString *signingID;
@property NSString *quarantineURL;
@property NSString *customMsg;
@property NSString *customURL;
@property BOOL silentBlock;
@end

View File

@@ -36,12 +36,20 @@ typedef NS_ENUM(NSInteger, SNTAction) {
#define RESPONSE_VALID(x) \
(x == SNTActionRespondAllow || x == SNTActionRespondDeny || x == SNTActionRespondAllowCompiler)
// Supported Rule Types
//
// Note: These enum values should be in order of decreasing precedence as
// evaluated by Santa. When adding new enum values, leave some space so that
// additional rules can be added without violating this. The ordering isn't
// strictly necessary but improves readability and may preemptively prevent
// issues should SQLite behavior change.
typedef NS_ENUM(NSInteger, SNTRuleType) {
SNTRuleTypeUnknown,
SNTRuleTypeUnknown = 0,
SNTRuleTypeBinary = 1,
SNTRuleTypeCertificate = 2,
SNTRuleTypeTeamID = 3,
SNTRuleTypeBinary = 1000,
SNTRuleTypeSigningID = 2000,
SNTRuleTypeCertificate = 3000,
SNTRuleTypeTeamID = 4000,
};
typedef NS_ENUM(NSInteger, SNTRuleState) {
@@ -63,32 +71,34 @@ typedef NS_ENUM(NSInteger, SNTClientMode) {
SNTClientModeLockdown = 2,
};
typedef NS_ENUM(NSInteger, SNTEventState) {
typedef NS_ENUM(uint64_t, SNTEventState) {
// Bits 0-15 bits store non-decision types
SNTEventStateUnknown = 0,
SNTEventStateBundleBinary = 1,
// Bits 16-23 store deny decision types
SNTEventStateBlockUnknown = 1 << 16,
SNTEventStateBlockBinary = 1 << 17,
SNTEventStateBlockCertificate = 1 << 18,
SNTEventStateBlockScope = 1 << 19,
SNTEventStateBlockTeamID = 1 << 20,
SNTEventStateBlockLongPath = 1 << 21,
// Bits 16-39 store deny decision types
SNTEventStateBlockUnknown = 1ULL << 16,
SNTEventStateBlockBinary = 1ULL << 17,
SNTEventStateBlockCertificate = 1ULL << 18,
SNTEventStateBlockScope = 1ULL << 19,
SNTEventStateBlockTeamID = 1ULL << 20,
SNTEventStateBlockLongPath = 1ULL << 21,
SNTEventStateBlockSigningID = 1ULL << 22,
// Bits 24-31 store allow decision types
SNTEventStateAllowUnknown = 1 << 24,
SNTEventStateAllowBinary = 1 << 25,
SNTEventStateAllowCertificate = 1 << 26,
SNTEventStateAllowScope = 1 << 27,
SNTEventStateAllowCompiler = 1 << 28,
SNTEventStateAllowTransitive = 1 << 29,
SNTEventStateAllowPendingTransitive = 1 << 30,
SNTEventStateAllowTeamID = 1 << 31,
// Bits 40-63 store allow decision types
SNTEventStateAllowUnknown = 1ULL << 40,
SNTEventStateAllowBinary = 1ULL << 41,
SNTEventStateAllowCertificate = 1ULL << 42,
SNTEventStateAllowScope = 1ULL << 43,
SNTEventStateAllowCompiler = 1ULL << 44,
SNTEventStateAllowTransitive = 1ULL << 45,
SNTEventStateAllowPendingTransitive = 1ULL << 46,
SNTEventStateAllowTeamID = 1ULL << 47,
SNTEventStateAllowSigningID = 1ULL << 48,
// Block and Allow masks
SNTEventStateBlock = 0xFF << 16,
SNTEventStateAllow = 0xFF << 24
SNTEventStateBlock = 0xFFFFFFULL << 16,
SNTEventStateAllow = 0xFFFFFFULL << 40,
};
typedef NS_ENUM(NSInteger, SNTRuleTableError) {
@@ -111,6 +121,7 @@ typedef NS_ENUM(NSInteger, SNTEventLogType) {
SNTEventLogTypeSyslog,
SNTEventLogTypeFilelog,
SNTEventLogTypeProtobuf,
SNTEventLogTypeJSON,
SNTEventLogTypeNull,
};

View File

@@ -390,6 +390,27 @@
///
@property(readonly, nonatomic) NSDictionary *syncProxyConfig;
///
/// Extra headers to include in all requests made during syncing.
/// Keys and values must all be strings, any other type will be silently ignored.
/// Some headers cannot be set through this key, including:
///
/// * Content-Encoding
/// * Content-Length
/// * Content-Type
/// * Connection
/// * Host
/// * Proxy-Authenticate
/// * Proxy-Authorization
/// * WWW-Authenticate
///
/// The header "Authorization" is also documented by Apple to be one that will
/// be ignored but this is not really the case, at least at present. If you
/// are able to use a different header for this that would be safest but if not
/// using Authorization /should/ be fine.
///
@property(readonly, nonatomic) NSDictionary *syncExtraHeaders;
///
/// The machine owner.
///

View File

@@ -57,6 +57,7 @@ static NSString *const kMobileConfigDomain = @"com.google.santa";
static NSString *const kStaticRules = @"StaticRules";
static NSString *const kSyncBaseURLKey = @"SyncBaseURL";
static NSString *const kSyncProxyConfigKey = @"SyncProxyConfiguration";
static NSString *const kSyncExtraHeadersKey = @"SyncExtraHeaders";
static NSString *const kSyncEnableCleanSyncEventUpload = @"SyncEnableCleanSyncEventUpload";
static NSString *const kClientAuthCertificateFileKey = @"ClientAuthCertificateFile";
static NSString *const kClientAuthCertificatePasswordKey = @"ClientAuthCertificatePassword";
@@ -194,7 +195,9 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kModeNotificationLockdown : string,
kStaticRules : array,
kSyncBaseURLKey : string,
kSyncEnableCleanSyncEventUpload : number,
kSyncProxyConfigKey : dictionary,
kSyncExtraHeadersKey : dictionary,
kClientAuthCertificateFileKey : string,
kClientAuthCertificatePasswordKey : string,
kClientAuthCertificateCNKey : string,
@@ -314,6 +317,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingSyncExtraHeaders {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEnableCleanSyncEventUpload {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEnablePageZeroProtection {
return [self configStateSet];
}
@@ -627,6 +638,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return self.configState[kSyncProxyConfigKey];
}
- (NSDictionary *)syncExtraHeaders {
return self.configState[kSyncExtraHeadersKey];
}
- (BOOL)enablePageZeroProtection {
NSNumber *number = self.configState[kEnablePageZeroProtectionKey];
return number ? [number boolValue] : YES;
@@ -794,6 +809,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return SNTEventLogTypeSyslog;
} else if ([logType isEqualToString:@"null"]) {
return SNTEventLogTypeNull;
} else if ([logType isEqualToString:@"json"]) {
return SNTEventLogTypeJSON;
} else if ([logType isEqualToString:@"file"]) {
return SNTEventLogTypeFilelog;
} else {

View File

@@ -0,0 +1,83 @@
/// Copyright 2023 Google LLC
///
/// 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
///
/// https://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 <Foundation/Foundation.h>
#import <MOLCertificate/MOLCertificate.h>
///
/// Represents an event stored in the database.
///
@interface SNTFileAccessEvent : NSObject <NSSecureCoding>
///
/// The watched path that was accessed
///
@property NSString *accessedPath;
///
/// The rule version and name that were violated
///
@property NSString *ruleVersion;
@property NSString *ruleName;
///
/// The SHA256 of the process that accessed the path
///
@property NSString *fileSHA256;
///
/// The path of the process that accessed the watched path
///
@property NSString *filePath;
///
/// If the process is part of a bundle, the name of the application
///
@property NSString *application;
///
/// If the executed file was signed, this is the Team ID if present in the signature information.
///
@property NSString *teamID;
///
/// If the executed file was signed, this is the Signing ID if present in the signature information.
///
@property NSString *signingID;
///
/// The user who executed the binary.
///
@property NSString *executingUser;
///
/// The process ID of the binary being executed.
///
@property NSNumber *pid;
///
/// The parent process ID of the binary being executed.
///
@property NSNumber *ppid;
///
/// The name of the parent process.
///
@property NSString *parentName;
// TODO(mlw): Store signing chain info
// @property NSArray<MOLCertificate*> *signingChain;
@end

View File

@@ -0,0 +1,79 @@
/// Copyright 2023 Google LLC
///
/// 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
///
/// https://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 "Source/common/SNTFileAccessEvent.h"
@implementation SNTFileAccessEvent
#define ENCODE(o) \
do { \
if (self.o) { \
[coder encodeObject:self.o forKey:@(#o)]; \
} \
} while (0)
#define DECODE(o, c) \
do { \
_##o = [decoder decodeObjectOfClass:[c class] forKey:@(#o)]; \
} while (0)
- (instancetype)init {
self = [super init];
if (self) {
}
return self;
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (void)encodeWithCoder:(NSCoder *)coder {
ENCODE(accessedPath);
ENCODE(ruleVersion);
ENCODE(ruleName);
ENCODE(fileSHA256);
ENCODE(filePath);
ENCODE(application);
ENCODE(teamID);
ENCODE(teamID);
ENCODE(pid);
ENCODE(ppid);
ENCODE(parentName);
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
self = [super init];
if (self) {
DECODE(accessedPath, NSString);
DECODE(ruleVersion, NSString);
DECODE(ruleName, NSString);
DECODE(fileSHA256, NSString);
DECODE(filePath, NSString);
DECODE(application, NSString);
DECODE(teamID, NSString);
DECODE(teamID, NSString);
DECODE(pid, NSNumber);
DECODE(ppid, NSNumber);
DECODE(parentName, NSString);
}
return self;
}
- (NSString *)description {
return [NSString
stringWithFormat:@"SNTFileAccessEvent: Accessed: %@, By: %@", self.accessedPath, self.filePath];
}
@end

View File

@@ -41,6 +41,11 @@
///
@property(copy) NSString *customMsg;
///
/// A custom URL to take the user to when this binary is blocked from executing.
///
@property(copy) NSString *customURL;
///
/// The time when this rule was last retrieved from the rules database, if rule is transitive.
/// Stored as number of seconds since 00:00:00 UTC on 1 January 2001.

View File

@@ -13,8 +13,15 @@
/// limitations under the License.
#import "Source/common/SNTRule.h"
#include <CommonCrypto/CommonCrypto.h>
#include <os/base.h>
#import "Source/common/SNTSyncConstants.h"
// https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/
static const NSUInteger kExpectedTeamIDLength = 10;
@interface SNTRule ()
@property(readwrite) NSUInteger timestamp;
@end
@@ -28,20 +35,84 @@
timestamp:(NSUInteger)timestamp {
self = [super init];
if (self) {
if (identifier.length == 0) {
return nil;
}
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdef"] invertedSet];
NSCharacterSet *nonUppercaseAlphaNumeric = [[NSCharacterSet
characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"] invertedSet];
switch (type) {
case SNTRuleTypeBinary: OS_FALLTHROUGH;
case SNTRuleTypeCertificate: {
// For binary and certificate rules, force the hash identifier to be lowercase hex.
identifier = [identifier lowercaseString];
identifier = [identifier stringByTrimmingCharactersInSet:nonHex];
if (identifier.length != (CC_SHA256_DIGEST_LENGTH * 2)) {
return nil;
}
break;
}
case SNTRuleTypeTeamID: {
// TeamIDs are always [0-9A-Z], so enforce that the identifier is uppercase
identifier =
[[identifier uppercaseString] stringByTrimmingCharactersInSet:nonUppercaseAlphaNumeric];
if (identifier.length != kExpectedTeamIDLength) {
return nil;
}
break;
}
case SNTRuleTypeSigningID: {
// SigningID rules are a combination of `TeamID:SigningID`. The TeamID should
// be forced to be uppercase, but because very loose rules exist for SigningIDs,
// their case will be kept as-is. However, platform binaries are expected to
// have the hardcoded string "platform" as the team ID and the case will be left
// as is.
NSArray *sidComponents = [identifier componentsSeparatedByString:@":"];
if (!sidComponents || sidComponents.count < 2) {
return nil;
}
// The first component is the TeamID
NSString *teamID = sidComponents[0];
if (![teamID isEqualToString:@"platform"]) {
teamID =
[[teamID uppercaseString] stringByTrimmingCharactersInSet:nonUppercaseAlphaNumeric];
if (teamID.length != kExpectedTeamIDLength) {
return nil;
}
}
// The rest of the components are the Signing ID since ":" a legal character.
// Join all but the last element of the components to rebuild the SigningID.
NSString *signingID = [[sidComponents
subarrayWithRange:NSMakeRange(1, sidComponents.count - 1)] componentsJoinedByString:@":"];
if (signingID.length == 0) {
return nil;
}
identifier = [NSString stringWithFormat:@"%@:%@", teamID, signingID];
break;
}
default: {
break;
}
}
_identifier = identifier;
_state = state;
_type = type;
_customMsg = customMsg;
_timestamp = timestamp;
if (_type == SNTRuleTypeBinary || _type == SNTRuleTypeCertificate) {
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
if ([[_identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length != 64)
return nil;
} else if (_identifier.length == 0) {
return nil;
}
}
return self;
}
@@ -100,6 +171,8 @@
type = SNTRuleTypeCertificate;
} else if ([ruleTypeString isEqual:kRuleTypeTeamID]) {
type = SNTRuleTypeTeamID;
} else if ([ruleTypeString isEqual:kRuleTypeSigningID]) {
type = SNTRuleTypeSigningID;
} else {
return nil;
}
@@ -109,7 +182,14 @@
customMsg = nil;
}
return [self initWithIdentifier:identifier state:state type:type customMsg:customMsg];
NSString *customURL = dict[kRuleCustomURL];
if (![customURL isKindOfClass:[NSString class]] || customURL.length == 0) {
customURL = nil;
}
SNTRule *r = [self initWithIdentifier:identifier state:state type:type customMsg:customMsg];
r.customURL = customURL;
return r;
}
#pragma mark NSSecureCoding
@@ -129,6 +209,7 @@
ENCODE(@(self.state), @"state");
ENCODE(@(self.type), @"type");
ENCODE(self.customMsg, @"custommsg");
ENCODE(self.customURL, @"customurl");
ENCODE(@(self.timestamp), @"timestamp");
}
@@ -139,6 +220,7 @@
_state = [DECODE(NSNumber, @"state") intValue];
_type = [DECODE(NSNumber, @"type") intValue];
_customMsg = DECODE(NSString, @"custommsg");
_customURL = DECODE(NSString, @"customurl");
_timestamp = [DECODE(NSNumber, @"timestamp") unsignedIntegerValue];
}
return self;

View File

@@ -13,6 +13,7 @@
/// limitations under the License.
#import <XCTest/XCTest.h>
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTRule.h"
@@ -46,13 +47,25 @@
XCTAssertEqual(sut.type, SNTRuleTypeCertificate);
XCTAssertEqual(sut.state, SNTRuleStateBlock);
// Ensure a Binary and Certificate rules properly convert identifiers to lowercase.
for (NSString *ruleType in @[ @"BINARY", @"CERTIFICATE" ]) {
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"B7C1E3FD640C5F211C89B02C2C6122F78CE322AA5C56EB0BB54BC422A8F8B670",
@"policy" : @"BLOCKLIST",
@"rule_type" : ruleType,
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
}
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"ABCDEFGHIJ",
@"policy" : @"SILENT_BLOCKLIST",
@"rule_type" : @"TEAMID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
XCTAssertEqual(sut.state, SNTRuleStateSilentBlock);
@@ -68,26 +81,112 @@
XCTAssertEqual(sut.state, SNTRuleStateAllowCompiler);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"ABCDEFGHIJ",
@"policy" : @"REMOVE",
@"rule_type" : @"TEAMID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
XCTAssertEqual(sut.state, SNTRuleStateRemove);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"ABCDEFGHIJ",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
@"custom_msg" : @"A custom block message",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
XCTAssertEqual(sut.state, SNTRuleStateAllow);
XCTAssertEqualObjects(sut.customMsg, @"A custom block message");
// TeamIDs must be 10 chars in length
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"A",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
}];
XCTAssertNil(sut);
// TeamIDs must be only alphanumeric chars
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"ßßßßßßßßßß",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
}];
XCTAssertNil(sut);
// TeamIDs are converted to uppercase
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"abcdefghij",
@"policy" : @"REMOVE",
@"rule_type" : @"TEAMID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
// SigningID tests
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"ABCDEFGHIJ:com.example",
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ:com.example");
XCTAssertEqual(sut.type, SNTRuleTypeSigningID);
XCTAssertEqual(sut.state, SNTRuleStateRemove);
// Invalid SingingID tests:
for (NSString *ident in @[
@":com.example", // missing team ID
@"ABCDEFGHIJ:", // missing signing ID
@"ABC:com.example", // Invalid team id
@":", // missing team and signing IDs
@"", // empty string
]) {
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : ident,
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNil(sut);
}
// Signing ID with lower team ID has case fixed up
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"abcdefghij:com.example",
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ:com.example");
// Signing ID with lower platform team ID is left alone
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"platform:com.example",
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"platform:com.example");
// Signing ID can contain the TID:SID delimiter character (":")
for (NSString *ident in @[
@"ABCDEFGHIJ:com:",
@"ABCDEFGHIJ:com:example",
@"ABCDEFGHIJ::",
@"ABCDEFGHIJ:com:example:with:more:components:",
]) {
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : ident,
@"policy" : @"ALLOWLIST",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, ident);
}
}
- (void)testInitWithDictionaryInvalid {

View File

@@ -100,6 +100,11 @@
///
@property NSString *teamID;
///
/// If the executed file was signed, this is the Signing ID if present in the signature information.
///
@property NSString *signingID;
///
/// The user who executed the binary.
///

View File

@@ -50,6 +50,7 @@
ENCODE(self.signingChain, @"signingChain");
ENCODE(self.teamID, @"teamID");
ENCODE(self.signingID, @"signingID");
ENCODE(self.executingUser, @"executingUser");
ENCODE(self.occurrenceDate, @"occurrenceDate");
@@ -95,10 +96,11 @@
_signingChain = DECODEARRAY(MOLCertificate, @"signingChain");
_teamID = DECODE(NSString, @"teamID");
_signingID = DECODE(NSString, @"signingID");
_executingUser = DECODE(NSString, @"executingUser");
_occurrenceDate = DECODE(NSDate, @"occurrenceDate");
_decision = (SNTEventState)[DECODE(NSNumber, @"decision") intValue];
_decision = (SNTEventState)[DECODE(NSNumber, @"decision") unsignedLongLongValue];
_pid = DECODE(NSNumber, @"pid");
_ppid = DECODE(NSNumber, @"ppid");
_parentName = DECODE(NSString, @"parentName");

View File

@@ -42,6 +42,7 @@ extern NSString *const kCertificateRuleCount;
extern NSString *const kCompilerRuleCount;
extern NSString *const kTransitiveRuleCount;
extern NSString *const kTeamIDRuleCount;
extern NSString *const kSigningIDRuleCount;
extern NSString *const kFullSyncInterval;
extern NSString *const kFCMToken;
extern NSString *const kFCMFullSyncInterval;
@@ -66,11 +67,13 @@ extern NSString *const kDecisionAllowBinary;
extern NSString *const kDecisionAllowCertificate;
extern NSString *const kDecisionAllowScope;
extern NSString *const kDecisionAllowTeamID;
extern NSString *const kDecisionAllowSigningID;
extern NSString *const kDecisionBlockUnknown;
extern NSString *const kDecisionBlockBinary;
extern NSString *const kDecisionBlockCertificate;
extern NSString *const kDecisionBlockScope;
extern NSString *const kDecisionBlockTeamID;
extern NSString *const kDecisionBlockSigningID;
extern NSString *const kDecisionUnknown;
extern NSString *const kDecisionBundleBinary;
extern NSString *const kLoggedInUsers;
@@ -95,6 +98,7 @@ extern NSString *const kCertOU;
extern NSString *const kCertValidFrom;
extern NSString *const kCertValidUntil;
extern NSString *const kTeamID;
extern NSString *const kSigningID;
extern NSString *const kQuarantineDataURL;
extern NSString *const kQuarantineRefererURL;
extern NSString *const kQuarantineTimestamp;
@@ -118,7 +122,9 @@ extern NSString *const kRuleType;
extern NSString *const kRuleTypeBinary;
extern NSString *const kRuleTypeCertificate;
extern NSString *const kRuleTypeTeamID;
extern NSString *const kRuleTypeSigningID;
extern NSString *const kRuleCustomMsg;
extern NSString *const kRuleCustomURL;
extern NSString *const kCursor;
extern NSString *const kBackoffInterval;

View File

@@ -42,6 +42,7 @@ NSString *const kCertificateRuleCount = @"certificate_rule_count";
NSString *const kCompilerRuleCount = @"compiler_rule_count";
NSString *const kTransitiveRuleCount = @"transitive_rule_count";
NSString *const kTeamIDRuleCount = @"teamid_rule_count";
NSString *const kSigningIDRuleCount = @"signingid_rule_count";
NSString *const kFullSyncInterval = @"full_sync_interval";
NSString *const kFCMToken = @"fcm_token";
NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval";
@@ -67,11 +68,13 @@ NSString *const kDecisionAllowBinary = @"ALLOW_BINARY";
NSString *const kDecisionAllowCertificate = @"ALLOW_CERTIFICATE";
NSString *const kDecisionAllowScope = @"ALLOW_SCOPE";
NSString *const kDecisionAllowTeamID = @"ALLOW_TEAMID";
NSString *const kDecisionAllowSigningID = @"ALLOW_SIGNINGID";
NSString *const kDecisionBlockUnknown = @"BLOCK_UNKNOWN";
NSString *const kDecisionBlockBinary = @"BLOCK_BINARY";
NSString *const kDecisionBlockCertificate = @"BLOCK_CERTIFICATE";
NSString *const kDecisionBlockScope = @"BLOCK_SCOPE";
NSString *const kDecisionBlockTeamID = @"BLOCK_TEAMID";
NSString *const kDecisionBlockSigningID = @"BLOCK_SIGNINGID";
NSString *const kDecisionUnknown = @"UNKNOWN";
NSString *const kDecisionBundleBinary = @"BUNDLE_BINARY";
NSString *const kLoggedInUsers = @"logged_in_users";
@@ -96,6 +99,7 @@ NSString *const kCertOU = @"ou";
NSString *const kCertValidFrom = @"valid_from";
NSString *const kCertValidUntil = @"valid_until";
NSString *const kTeamID = @"team_id";
NSString *const kSigningID = @"signing_id";
NSString *const kQuarantineDataURL = @"quarantine_data_url";
NSString *const kQuarantineRefererURL = @"quarantine_referer_url";
NSString *const kQuarantineTimestamp = @"quarantine_timestamp";
@@ -119,7 +123,9 @@ NSString *const kRuleType = @"rule_type";
NSString *const kRuleTypeBinary = @"BINARY";
NSString *const kRuleTypeCertificate = @"CERTIFICATE";
NSString *const kRuleTypeTeamID = @"TEAMID";
NSString *const kRuleTypeSigningID = @"SIGNINGID";
NSString *const kRuleCustomMsg = @"custom_msg";
NSString *const kRuleCustomURL = @"custom_url";
NSString *const kCursor = @"cursor";
NSString *const kBackoffInterval = @"backoff";

View File

@@ -35,6 +35,7 @@
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID
signingID:(NSString *)signingID
reply:(void (^)(SNTRule *))reply;
///

View File

@@ -17,13 +17,18 @@
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTXPCBundleServiceInterface.h"
@class SNTStoredEvent;
@class SNTDeviceEvent;
@class SNTFileAccessEvent;
@class SNTStoredEvent;
/// Protocol implemented by SantaGUI and utilized by santad
@protocol SNTNotifierXPC
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message;
- (void)postBlockNotification:(SNTStoredEvent *)event
withCustomMessage:(NSString *)message
andCustomURL:(NSString *)url;
- (void)postUSBBlockNotification:(SNTDeviceEvent *)event withCustomMessage:(NSString *)message;
- (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
withCustomMessage:(NSString *)message API_AVAILABLE(macos(13.0));
- (void)postClientModeNotification:(SNTClientMode)clientmode;
- (void)postRuleSyncNotificationWithCustomMessage:(NSString *)message;
- (void)updateCountsForEvent:(SNTStoredEvent *)event

View File

@@ -37,7 +37,7 @@
/// Database ops
///
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
int64_t transitive, int64_t teamID))reply;
int64_t transitive, int64_t teamID, int64_t signingID))reply;
- (void)databaseEventCount:(void (^)(int64_t count))reply;
- (void)staticRuleCount:(void (^)(int64_t count))reply;
@@ -57,6 +57,7 @@
fileSHA256:(NSString *)fileSHA256
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID
signingID:(NSString *)signingID
reply:(void (^)(SNTEventState))reply;
///

View File

@@ -30,6 +30,14 @@ static inline std::string NSStringToUTF8String(NSString *str) {
return std::string(str.UTF8String, [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
}
static inline NSString *StringToNSString(const std::string &str) {
return [NSString stringWithUTF8String:str.c_str()];
}
static inline NSString *StringToNSString(const char *str) {
return [NSString stringWithUTF8String:str];
}
} // namespace santa::common
#endif

View File

@@ -38,23 +38,29 @@
// Pretty print C++ string match errors
#define XCTAssertCppStringEqual(got, want) XCTAssertCStringEqual((got).c_str(), (want).c_str())
// Note: Delta between local formatter and the one run on Github. Disable for now.
// clang-format off
#define XCTAssertSemaTrue(s, sec, m) \
XCTAssertEqual( \
0, dispatch_semaphore_wait((s), dispatch_time(DISPATCH_TIME_NOW, (sec)*NSEC_PER_SEC)), m)
0, dispatch_semaphore_wait((s), dispatch_time(DISPATCH_TIME_NOW, (sec) * NSEC_PER_SEC)), m)
// clang-format on
// Helper to ensure at least `ms` milliseconds are slept, even if the sleep
// function returns early due to interrupts.
void SleepMS(long ms);
enum class ActionType {
Auth,
Notify,
};
// Helper to construct strings of a given length
NSString *RepeatedString(NSString *str, NSUInteger len);
//
// Helpers to construct various ES structs
//
enum class ActionType {
Auth,
Notify,
};
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver);
/// Construct a `struct stat` buffer with each member having a unique value.
@@ -64,7 +70,7 @@ audit_token_t MakeAuditToken(pid_t pid, pid_t pidver);
struct stat MakeStat(int offset = 0);
es_string_token_t MakeESStringToken(const char *s);
es_file_t MakeESFile(const char *path, struct stat sb = {});
es_file_t MakeESFile(const char *path, struct stat sb = MakeStat());
es_process_t MakeESProcess(es_file_t *file, audit_token_t tok = {}, audit_token_t parent_tok = {});
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc,
ActionType action_type = ActionType::Notify,

View File

@@ -21,6 +21,10 @@
#include <uuid/uuid.h>
#include "Source/common/SystemResources.h"
NSString *RepeatedString(NSString *str, NSUInteger len) {
return [@"" stringByPaddingToLength:len withString:str startingAtIndex:0];
}
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver) {
return audit_token_t{
.val =

View File

@@ -262,6 +262,7 @@ message Execution {
REASON_TRANSITIVE = 8;
REASON_LONG_PATH = 9;
REASON_NOT_RUNNING = 10;
REASON_SIGNING_ID = 11;
}
optional Reason reason = 10;

View File

@@ -31,6 +31,17 @@ swift_library(
],
)
swift_library(
name = "SNTFileAccessMessageWindowView",
srcs = [
"SNTFileAccessMessageWindowView.swift",
],
generates_header = 1,
deps = [
"//Source/common:SNTFileAccessEvent",
],
)
objc_library(
name = "SantaGUI_lib",
srcs = [
@@ -44,6 +55,8 @@ objc_library(
"SNTBinaryMessageWindowController.m",
"SNTDeviceMessageWindowController.h",
"SNTDeviceMessageWindowController.m",
"SNTFileAccessMessageWindowController.h",
"SNTFileAccessMessageWindowController.m",
"SNTMessageWindowController.h",
"SNTMessageWindowController.m",
"SNTNotificationManager.h",
@@ -65,9 +78,11 @@ objc_library(
deps = [
":SNTAboutWindowView",
":SNTDeviceMessageWindowView",
":SNTFileAccessMessageWindowView",
"//Source/common:SNTBlockMessage_SantaGUI",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDeviceEvent",
"//Source/common:SNTFileAccessEvent",
"//Source/common:SNTLogging",
"//Source/common:SNTStoredEvent",
"//Source/common:SNTStrengthify",

View File

@@ -23,7 +23,9 @@
///
@interface SNTBinaryMessageWindowController : SNTMessageWindowController
- (instancetype)initWithEvent:(SNTStoredEvent *)event andMessage:(NSString *)message;
- (instancetype)initWithEvent:(SNTStoredEvent *)event
customMsg:(NSString *)message
customURL:(NSString *)url;
- (IBAction)showCertInfo:(id)sender;
- (void)updateBlockNotification:(SNTStoredEvent *)event withBundleHash:(NSString *)bundleHash;

View File

@@ -25,6 +25,9 @@
/// The custom message to display for this event
@property(copy) NSString *customMessage;
/// The custom URL to use for this event
@property(copy) NSString *customURL;
/// A 'friendly' string representing the certificate information
@property(readonly, nonatomic) NSString *publisherInfo;
@@ -39,11 +42,14 @@
@implementation SNTBinaryMessageWindowController
- (instancetype)initWithEvent:(SNTStoredEvent *)event andMessage:(NSString *)message {
- (instancetype)initWithEvent:(SNTStoredEvent *)event
customMsg:(NSString *)message
customURL:(NSString *)url {
self = [super initWithWindowNibName:@"MessageWindow"];
if (self) {
_event = event;
_customMessage = message;
_customURL = url;
_progress = [NSProgress discreteProgressWithTotalUnitCount:1];
[_progress addObserver:self
forKeyPath:@"fractionCompleted"
@@ -74,7 +80,9 @@
- (void)loadWindow {
[super loadWindow];
if (![[SNTConfigurator configurator] eventDetailURL]) {
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event customURL:self.customURL];
if (!url) {
[self.openEventButton removeFromSuperview];
} else {
NSString *eventDetailText = [[SNTConfigurator configurator] eventDetailText];
@@ -120,7 +128,8 @@
}
- (IBAction)openEventDetails:(id)sender {
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event];
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event customURL:self.customURL];
[self closeWindow:sender];
[[NSWorkspace sharedWorkspace] openURL:url];
}

View File

@@ -1,3 +1,17 @@
/// Copyright 2023 Google LLC
///
/// 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
///
/// https://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 SwiftUI
import santa_common_SNTConfigurator

View File

@@ -0,0 +1,35 @@
/// Copyright 2023 Google LLC
///
/// 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
///
/// https://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 <Cocoa/Cocoa.h>
#import "Source/gui/SNTMessageWindowController.h"
NS_ASSUME_NONNULL_BEGIN
@class SNTFileAccessEvent;
///
/// Controller for a single message window.
///
API_AVAILABLE(macos(13.0))
@interface SNTFileAccessMessageWindowController : SNTMessageWindowController <NSWindowDelegate>
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event message:(nullable NSString *)message;
@property(readonly) SNTFileAccessEvent *event;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,79 @@
/// Copyright 2023 Google LLC
///
/// 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
///
/// https://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 "Source/gui/SNTFileAccessMessageWindowController.h"
#import "Source/gui/SNTFileAccessMessageWindowView-Swift.h"
#import "Source/common/SNTBlockMessage.h"
#import "Source/common/SNTFileAccessEvent.h"
#import "Source/common/SNTLogging.h"
@interface SNTFileAccessMessageWindowController ()
@property NSString *customMessage;
@property SNTFileAccessEvent *event;
@end
@implementation SNTFileAccessMessageWindowController
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event message:(nullable NSString *)message {
self = [super init];
if (self) {
_customMessage = message;
_event = event;
}
return self;
}
- (void)showWindow:(id)sender {
if (self.window) {
[self.window orderOut:sender];
}
self.window =
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
styleMask:NSWindowStyleMaskClosable | NSWindowStyleMaskTitled
backing:NSBackingStoreBuffered
defer:NO];
self.window.contentViewController =
[SNTFileAccessMessageWindowViewFactory createWithWindow:self.window
event:self.event
customMsg:self.attributedCustomMessage];
self.window.delegate = self;
// Add app to Cmd+Tab and Dock.
NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
[super showWindow:sender];
}
- (void)windowWillClose:(NSNotification *)notification {
// Remove app from Cmd+Tab and Dock.
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
[super windowWillClose:notification];
}
- (NSAttributedString *)attributedCustomMessage {
return [SNTBlockMessage formatMessage:self.customMessage];
}
- (NSString *)messageHash {
// TODO(mlw): This is not the final form. As this feature is expanded this
// hash will need to be revisted to ensure it meets our needs.
return [NSString stringWithFormat:@"%@|%@|%d", self.event.ruleName, self.event.ruleVersion,
[self.event.pid intValue]];
}
@end

View File

@@ -0,0 +1,158 @@
/// Copyright 2023 Google LLC
///
/// 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
///
/// https://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 SwiftUI
import santa_common_SNTFileAccessEvent
@available(macOS 13, *)
@objc public class SNTFileAccessMessageWindowViewFactory : NSObject {
@objc public static func createWith(window: NSWindow, event: SNTFileAccessEvent, customMsg: NSAttributedString?) -> NSViewController {
return NSHostingController(rootView:SNTFileAccessMessageWindowView(window:window, event:event, customMsg:customMsg)
.frame(width:800, height:600))
}
}
@available(macOS 13, *)
struct Property : View {
var lbl: String
var val: String
var body: some View {
let width: CGFloat? = 150
HStack(spacing: 5) {
Text(lbl + ":")
.frame(width: width, alignment: .trailing)
.lineLimit(1)
.font(.system(size: 12, weight: .bold))
.padding(Edge.Set.horizontal, 10)
Text(val)
.fixedSize(horizontal: false, vertical: true)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
}
}
@available(macOS 13, *)
struct Event: View {
let e: SNTFileAccessEvent
var body: some View {
VStack(spacing:10) {
Property(lbl: "Path Accessed", val: e.accessedPath)
Property(lbl: "Rule Name", val: e.ruleName)
Property(lbl: "Rule Version", val: e.ruleVersion)
Divider()
.frame(width: 700)
if let app = e.application {
Property(lbl: "Application", val: app)
}
Property(lbl: "Name", val: (e.filePath as NSString).lastPathComponent)
Property(lbl: "Path", val: e.filePath)
Property(lbl: "Identifier", val: e.fileSHA256)
Property(lbl: "Parent", val: e.parentName + " (" + e.ppid.stringValue + ")")
}
}
}
@available(macOS 13, *)
struct SNTFileAccessMessageWindowView: View {
let window: NSWindow?
let event: SNTFileAccessEvent?
let customMsg: NSAttributedString?
@State private var checked = false
var body: some View {
VStack(spacing:20.0) {
Spacer()
Text("Santa").font(Font.custom("HelveticaNeue-UltraLight", size: 34.0))
if let msg = customMsg {
Text(AttributedString(msg)).multilineTextAlignment(.center).padding(15.0)
} else {
Text("Access to a protected resource was denied.").multilineTextAlignment(.center).padding(15.0)
}
Event(e: event!)
Toggle(isOn: $checked) {
Text("Prevent future notifications for this application for a day")
.font(Font.system(size: 11.0));
}
VStack(spacing:15) {
Button(action: openButton, label: {
Text("Open Event Info...").frame(maxWidth:.infinity)
})
Button(action: dismissButton, label: {
Text("Dismiss").frame(maxWidth:.infinity)
})
.keyboardShortcut(.return)
}.frame(width: 220)
Spacer()
}.frame(maxWidth:800.0).fixedSize()
}
func publisherInfo() {
// TODO(mlw): Will hook up in a separate PR
print("showing publisher popup...")
}
func openButton() {
// TODO(mlw): Will hook up in a separate PR
print("opening event info...")
}
func dismissButton() {
window?.close()
print("close window")
}
}
@available(macOS 13, *)
func testFileAccessEvent() -> SNTFileAccessEvent {
let faaEvent = SNTFileAccessEvent()
faaEvent.accessedPath = "/accessed/path"
faaEvent.ruleVersion = "watched_path.v1"
faaEvent.ruleName = "watched_path"
faaEvent.fileSHA256 = "b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
faaEvent.filePath = "/Applications/gShoe.app/Contents/MacOS/gShoe"
faaEvent.application = "gShoe"
faaEvent.teamID = "EQHXZ8M8AV"
faaEvent.signingID = "com.google.gShoe"
faaEvent.executingUser = "nobody"
faaEvent.pid = 456
faaEvent.ppid = 123
faaEvent.parentName = "gLauncher"
return faaEvent
}
// Enable previews in Xcode.
@available(macOS 13, *)
struct SNTFileAccessMessageWindowView_Previews: PreviewProvider {
static var previews: some View {
SNTFileAccessMessageWindowView(window: nil, event: testFileAccessEvent(), customMsg: nil)
}
}

View File

@@ -15,8 +15,6 @@
#import <Cocoa/Cocoa.h>
#import "Source/common/SNTXPCNotifierInterface.h"
#import "Source/gui/SNTBinaryMessageWindowController.h"
#import "Source/gui/SNTDeviceMessageWindowController.h"
#import "Source/gui/SNTMessageWindowController.h"
///

View File

@@ -26,6 +26,9 @@
#import "Source/common/SNTStrengthify.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/gui/SNTBinaryMessageWindowController.h"
#import "Source/gui/SNTDeviceMessageWindowController.h"
#import "Source/gui/SNTFileAccessMessageWindowController.h"
#import "Source/gui/SNTMessageWindowController.h"
@interface SNTNotificationManager ()
@@ -169,7 +172,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
[dc postNotificationName:@"com.google.santa.notification.blockedeexecution"
object:@"com.google.santa"
userInfo:userInfo];
userInfo:userInfo
deliverImmediately:YES];
}
- (void)showQueuedWindow {
@@ -319,14 +323,16 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
[un addNotificationRequest:req withCompletionHandler:nil];
}
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message {
- (void)postBlockNotification:(SNTStoredEvent *)event
withCustomMessage:(NSString *)message
andCustomURL:(NSString *)url {
if (!event) {
LOGI(@"Error: Missing event object in message received from daemon!");
return;
}
SNTBinaryMessageWindowController *pendingMsg =
[[SNTBinaryMessageWindowController alloc] initWithEvent:event andMessage:message];
[[SNTBinaryMessageWindowController alloc] initWithEvent:event customMsg:message customURL:url];
[self queueMessage:pendingMsg];
}
@@ -342,6 +348,19 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
[self queueMessage:pendingMsg];
}
- (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
withCustomMessage:(NSString *)message API_AVAILABLE(macos(13.0)) {
if (!event) {
LOGI(@"Error: Missing event object in message received from daemon!");
return;
}
SNTFileAccessMessageWindowController *pendingMsg =
[[SNTFileAccessMessageWindowController alloc] initWithEvent:event message:message];
[self queueMessage:pendingMsg];
}
#pragma mark SNTBundleNotifierXPC protocol methods
- (void)updateCountsForEvent:(SNTStoredEvent *)event

View File

@@ -58,7 +58,7 @@
id dncMock = OCMClassMock([NSDistributedNotificationCenter class]);
OCMStub([dncMock defaultCenter]).andReturn(dncMock);
[sut postBlockNotification:ev withCustomMessage:@""];
[sut postBlockNotification:ev withCustomMessage:@"" andCustomURL:@""];
OCMVerify([dncMock postNotificationName:@"com.google.santa.notification.blockedeexecution"
object:@"com.google.santa"
@@ -68,7 +68,8 @@
XCTAssertEqualObjects(userInfo[@"ppid"], @1);
XCTAssertEqualObjects(userInfo[@"execution_time"], @1660221048);
return YES;
}]]);
}]
deliverImmediately:YES]);
}
@end

View File

@@ -42,6 +42,7 @@ static NSString *const kRule = @"Rule";
static NSString *const kSigningChain = @"Signing Chain";
static NSString *const kUniversalSigningChain = @"Universal Signing Chain";
static NSString *const kTeamID = @"Team ID";
static NSString *const kSigningID = @"Signing ID";
// signing chain keys
static NSString *const kCommonName = @"Common Name";
@@ -111,6 +112,7 @@ typedef id (^SNTAttributeBlock)(SNTCommandFileInfo *, SNTFileInfo *);
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadTimestamp;
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadAgent;
@property(readonly, copy, nonatomic) SNTAttributeBlock teamID;
@property(readonly, copy, nonatomic) SNTAttributeBlock signingID;
@property(readonly, copy, nonatomic) SNTAttributeBlock type;
@property(readonly, copy, nonatomic) SNTAttributeBlock pageZero;
@property(readonly, copy, nonatomic) SNTAttributeBlock codeSigned;
@@ -184,8 +186,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
+ (NSArray<NSString *> *)fileInfoKeys {
return @[
kPath, kSHA256, kSHA1, kBundleName, kBundleVersion, kBundleVersionStr, kDownloadReferrerURL,
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kType, kPageZero, kCodeSigned, kRule,
kSigningChain, kUniversalSigningChain
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kType, kPageZero,
kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
];
}
@@ -218,6 +220,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
kSigningChain : self.signingChain,
kUniversalSigningChain : self.universalSigningChain,
kTeamID : self.teamID,
kSigningID : self.signingID,
};
_printQueue = dispatch_queue_create("com.google.santactl.print_queue", DISPATCH_QUEUE_SERIAL);
@@ -357,15 +360,34 @@ REGISTER_COMMAND_NAME(@"fileinfo")
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSError *err;
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err];
[[cmd.daemonConn remoteObjectProxy]
decisionForFilePath:fileInfo.path
fileSHA256:fileInfo.SHA256
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
teamID:[csc.signingInformation valueForKey:@"teamid"]
reply:^(SNTEventState s) {
state = s;
dispatch_semaphore_signal(sema);
}];
NSString *teamID =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
NSString *identifier =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
NSString *signingID;
if (identifier) {
if (teamID) {
signingID = [NSString stringWithFormat:@"%@:%@", teamID, identifier];
} else {
id platformID =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
if ([platformID isKindOfClass:[NSNumber class]] && [platformID intValue] != 0) {
signingID = [NSString stringWithFormat:@"platform:%@", identifier];
}
}
}
[[cmd.daemonConn remoteObjectProxy] decisionForFilePath:fileInfo.path
fileSHA256:fileInfo.SHA256
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
teamID:teamID
signingID:signingID
reply:^(SNTEventState s) {
state = s;
dispatch_semaphore_signal(sema);
}];
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
cmd.daemonUnavailable = YES;
return kCommunicationErrorMsg;
@@ -381,6 +403,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
case SNTEventStateBlockCertificate: [output appendString:@" (Certificate)"]; break;
case SNTEventStateAllowTeamID:
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
case SNTEventStateAllowSigningID:
case SNTEventStateBlockSigningID: [output appendString:@" (SigningID)"]; break;
case SNTEventStateAllowScope:
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
@@ -473,6 +497,13 @@ REGISTER_COMMAND_NAME(@"fileinfo")
};
}
- (SNTAttributeBlock)signingID {
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
};
}
#pragma mark -
// Entry point for the command.

View File

@@ -60,16 +60,28 @@ REGISTER_COMMAND_NAME(@"rule")
@" Will add the hash of the file currently at that path.\n"
@" Does not work with --check. Use the fileinfo verb to check.\n"
@" the rule state of a file.\n"
@" --identifier {sha256|teamID}: identifier to add/remove/check\n"
@" --identifier {sha256|teamID|signingID}: identifier to add/remove/check\n"
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
@"\n"
@" Optionally:\n"
@" --teamid: add or check a team ID rule instead of binary\n"
@" --signingid: add or check a signing ID rule instead of binary (see notes)\n"
@" --certificate: add or check a certificate sha256 rule instead of binary\n"
#ifdef DEBUG
@" --force: allow manual changes even when SyncBaseUrl is set\n"
#endif
@" --message {message}: custom message\n");
@" --message {message}: custom message\n"
@"\n"
@" Notes:\n"
@" The format of `identifier` when adding/checking a `signingid` rule is:\n"
@"\n"
@" `TeamID:SigningID`\n"
@"\n"
@" Because signing IDs are controlled by the binary author, this ensures\n"
@" that the signing ID is properly scoped to a developer. For the special\n"
@" case of platform binaries, `TeamID` should be replaced with the string\n"
@" \"platform\" (e.g. `platform:SigningID`). This allows for rules\n"
@" targeting Apple-signed binaries that do not have a team ID.\n");
}
- (void)runWithArguments:(NSArray *)arguments {
@@ -116,6 +128,8 @@ REGISTER_COMMAND_NAME(@"rule")
newRule.type = SNTRuleTypeCertificate;
} else if ([arg caseInsensitiveCompare:@"--teamid"] == NSOrderedSame) {
newRule.type = SNTRuleTypeTeamID;
} else if ([arg caseInsensitiveCompare:@"--signingid"] == NSOrderedSame) {
newRule.type = SNTRuleTypeSigningID;
} else if ([arg caseInsensitiveCompare:@"--path"] == NSOrderedSame) {
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--path requires an argument"];
@@ -145,6 +159,22 @@ REGISTER_COMMAND_NAME(@"rule")
}
}
if (path) {
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:path];
if (!fi.path) {
[self printErrorUsageAndExit:@"Provided path was not a plain file"];
}
if (newRule.type == SNTRuleTypeBinary) {
newRule.identifier = fi.SHA256;
} else if (newRule.type == SNTRuleTypeCertificate) {
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
newRule.identifier = cs.leafCertificate.SHA256;
} else if (newRule.type == SNTRuleTypeTeamID || newRule.type == SNTRuleTypeSigningID) {
// noop
}
}
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) {
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
@@ -159,21 +189,6 @@ REGISTER_COMMAND_NAME(@"rule")
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
}
if (path) {
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:path];
if (!fi.path) {
[self printErrorUsageAndExit:@"Provided path was not a plain file"];
}
if (newRule.type == SNTRuleTypeBinary) {
newRule.identifier = fi.SHA256;
} else if (newRule.type == SNTRuleTypeCertificate) {
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
newRule.identifier = cs.leafCertificate.SHA256;
} else if (newRule.type == SNTRuleTypeTeamID) {
}
}
if (newRule.state == SNTRuleStateUnknown) {
[self printErrorUsageAndExit:@"No state specified"];
} else if (!newRule.identifier) {
@@ -220,11 +235,13 @@ REGISTER_COMMAND_NAME(@"rule")
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil;
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil;
NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil;
NSString *signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil;
__block NSMutableString *output;
[rop decisionForFilePath:nil
fileSHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID
signingID:signingID
reply:^(SNTEventState s) {
output =
(SNTEventStateAllow & s) ? @"Allowed".mutableCopy : @"Blocked".mutableCopy;
@@ -247,6 +264,10 @@ REGISTER_COMMAND_NAME(@"rule")
break;
case SNTEventStateAllowTeamID:
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
case SNTEventStateAllowSigningID:
case SNTEventStateBlockSigningID:
[output appendString:@" (SigningID)"];
break;
default: output = @"None".mutableCopy; break;
}
if (isatty(STDOUT_FILENO)) {
@@ -266,6 +287,7 @@ REGISTER_COMMAND_NAME(@"rule")
[rop databaseRuleForBinarySHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID
signingID:signingID
reply:^(SNTRule *r) {
if (r.state == SNTRuleStateAllowTransitive) {
NSDate *date =

View File

@@ -81,13 +81,19 @@ REGISTER_COMMAND_NAME(@"status")
}];
// Database counts
__block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1, teamIDRuleCount = -1;
__block int64_t compilerRuleCount = -1, transitiveRuleCount = -1;
__block int64_t eventCount = -1;
__block int64_t binaryRuleCount = -1;
__block int64_t certRuleCount = -1;
__block int64_t teamIDRuleCount = -1;
__block int64_t signingIDRuleCount = -1;
__block int64_t compilerRuleCount = -1;
__block int64_t transitiveRuleCount = -1;
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
int64_t transitive, int64_t teamID) {
int64_t transitive, int64_t teamID, int64_t signingID) {
binaryRuleCount = binary;
certRuleCount = certificate;
teamIDRuleCount = teamID;
signingIDRuleCount = signingID;
compilerRuleCount = compiler;
transitiveRuleCount = transitive;
}];
@@ -193,6 +199,8 @@ REGISTER_COMMAND_NAME(@"status")
@"database" : @{
@"binary_rules" : @(binaryRuleCount),
@"certificate_rules" : @(certRuleCount),
@"teamid_rules" : @(teamIDRuleCount),
@"signingid_rules" : @(signingIDRuleCount),
@"compiler_rules" : @(compilerRuleCount),
@"transitive_rules" : @(transitiveRuleCount),
@"events_pending_upload" : @(eventCount),
@@ -258,6 +266,7 @@ REGISTER_COMMAND_NAME(@"status")
printf(" %-25s | %lld\n", "Binary Rules", binaryRuleCount);
printf(" %-25s | %lld\n", "Certificate Rules", certRuleCount);
printf(" %-25s | %lld\n", "TeamID Rules", teamIDRuleCount);
printf(" %-25s | %lld\n", "SigningID Rules", signingIDRuleCount);
printf(" %-25s | %lld\n", "Compiler Rules", compilerRuleCount);
printf(" %-25s | %lld\n", "Transitive Rules", transitiveRuleCount);
printf(" %-25s | %lld\n", "Events Pending Upload", eventCount);

View File

@@ -209,6 +209,16 @@ objc_library(
],
)
objc_library(
name = "TTYWriter",
srcs = ["TTYWriter.mm"],
hdrs = ["TTYWriter.h"],
deps = [
"//Source/common:SNTLogging",
"//Source/common:String",
],
)
objc_library(
name = "SNTExecutionController",
srcs = ["SNTExecutionController.mm"],
@@ -221,6 +231,7 @@ objc_library(
":SNTPolicyProcessor",
":SNTRuleTable",
":SNTSyncdQueue",
":TTYWriter",
"//Source/common:BranchPrediction",
"//Source/common:SNTBlockMessage",
"//Source/common:SNTCachedDecision",
@@ -286,7 +297,9 @@ objc_library(
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
"//Source/common:PrefixTree",
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:String",
"//Source/common:Unit",
],
)
@@ -332,6 +345,9 @@ objc_library(
name = "SNTEndpointSecurityFileAccessAuthorizer",
srcs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.mm"],
hdrs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.h"],
sdk_dylibs = [
"bsm",
],
deps = [
":EndpointSecurityAPI",
":EndpointSecurityEnrichedTypes",
@@ -343,18 +359,23 @@ objc_library(
":SNTDecisionCache",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
":TTYWriter",
":WatchItemPolicy",
":WatchItems",
"//Source/common:Platform",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTFileAccessEvent",
"//Source/common:SNTMetricSet",
"//Source/common:SNTStrengthify",
"//Source/common:SantaCache",
"//Source/common:SantaVnode",
"//Source/common:SantaVnodeHash",
"//Source/common:String",
"@MOLCertificate",
"@MOLCodesignChecker",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/container:flat_hash_set",
],
)
@@ -486,7 +507,6 @@ objc_library(
":EndpointSecuritySerializerUtilities",
":SNTDecisionCache",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:SNTStoredEvent",
],
@@ -574,6 +594,7 @@ objc_library(
":EndpointSecurityWriterNull",
":EndpointSecurityWriterSpool",
":EndpointSecurityWriterSyslog",
":SNTDecisionCache",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTLogging",
"//Source/common:SNTStoredEvent",
@@ -674,10 +695,12 @@ objc_library(
":SNTExecutionController",
":SNTNotificationQueue",
":SNTSyncdQueue",
":TTYWriter",
":WatchItems",
"//Source/common:PrefixTree",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTFileAccessEvent",
"//Source/common:SNTKVOManager",
"//Source/common:SNTLogging",
"//Source/common:SNTXPCNotifierInterface",
@@ -699,11 +722,13 @@ objc_library(
":Metrics",
":SNTCompilerController",
":SNTDatabaseController",
":SNTDecisionCache",
":SNTEventTable",
":SNTExecutionController",
":SNTNotificationQueue",
":SNTRuleTable",
":SNTSyncdQueue",
":TTYWriter",
":WatchItems",
"//Source/common:PrefixTree",
"//Source/common:SNTConfigurator",
@@ -1256,6 +1281,7 @@ santa_unit_test(
":SNTCompilerController",
":SNTEndpointSecurityRecorder",
"//Source/common:PrefixTree",
"//Source/common:SNTConfigurator",
"//Source/common:TestUtils",
"//Source/common:Unit",
"@OCMock",

View File

@@ -39,9 +39,10 @@
bail = YES;
return;
}
[db close];
[[NSFileManager defaultManager] removeItemAtPath:[db databasePath] error:NULL];
[db open];
[self closeDeleteReopenDatabase:db];
} else if ([db userVersion] > [self currentSupportedVersion]) {
LOGW(@"Database version newer than supported. Deleting.");
[self closeDeleteReopenDatabase:db];
}
}];
@@ -58,11 +59,22 @@
return nil;
}
- (void)closeDeleteReopenDatabase:(FMDatabase *)db {
[db close];
[[NSFileManager defaultManager] removeItemAtPath:[db databasePath] error:NULL];
[db open];
}
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
[self doesNotRecognizeSelector:_cmd];
return 0;
}
- (uint32_t)currentSupportedVersion {
[self doesNotRecognizeSelector:_cmd];
return 0;
}
/// Called at the end of initialization to ensure the table in the
/// database exists and uses the latest schema.
- (void)updateTableSchema {

View File

@@ -18,8 +18,14 @@
#import "Source/common/SNTStoredEvent.h"
static const uint32_t kEventTableCurrentVersion = 3;
@implementation SNTEventTable
- (uint32_t)currentSupportedVersion {
return kEventTableCurrentVersion;
}
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
int newVersion = 0;

View File

@@ -57,10 +57,16 @@
- (NSUInteger)teamIDRuleCount;
///
/// @return Rule for binary or certificate with given SHA-256. The binary rule will be returned
/// if it exists. If not, the certificate rule will be returned if it exists.
/// @return Number of signing ID rules in the database
///
- (NSUInteger)signingIDRuleCount;
///
/// @return Rule for binary, signingID, certificate or teamID (in that order).
/// The first matching rule found is returned.
///
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
signingID:(NSString *)signingID
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID;

View File

@@ -25,6 +25,8 @@
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTRule.h"
static const uint32_t kRuleTableCurrentVersion = 7;
// TODO(nguyenphillip): this should be configurable.
// How many rules must be in database before we start trying to remove transitive rules.
static const NSUInteger kTransitiveRuleCullingThreshold = 500000;
@@ -173,6 +175,10 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
self.criticalSystemBinaries = bins;
}
- (uint32_t)currentSupportedVersion {
return kRuleTableCurrentVersion;
}
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
// Lock this database from other processes
[[db executeQuery:@"PRAGMA locking_mode = EXCLUSIVE;"] close];
@@ -204,12 +210,47 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
[db executeUpdate:@"ALTER TABLE 'rules' ADD 'timestamp' INTEGER"];
newVersion = 3;
}
if (version < 4) {
// Rename `shasum` column to `identifier`.
[db executeUpdate:@"ALTER TABLE 'rules' RENAME COLUMN 'shasum' TO 'identifier'"];
newVersion = 4;
}
if (version < 5) {
// Migrate SNTRuleType enum values
// Note: The reordering is intentional so that the type values are in order
// of precedence.
[db executeUpdate:@"UPDATE rules SET type = 1000 WHERE type = 1"];
[db executeUpdate:@"UPDATE rules SET type = 3000 WHERE type = 2"];
[db executeUpdate:@"UPDATE rules SET type = 4000 WHERE type = 3"];
[db executeUpdate:@"UPDATE rules SET type = 2000 WHERE type = 4"];
newVersion = 5;
}
if (version < 6) {
// Force hash identifiers for Binary and Certificate rules to always be lowercase
[db executeUpdate:@"UPDATE rules SET identifier = LOWER(identifier) WHERE type = ? OR type = ?",
@(SNTRuleTypeBinary), @(SNTRuleTypeCertificate)];
// Force team ID identifiers for TeamID rules to always be uppercase
[db executeUpdate:@"UPDATE rules SET identifier = UPPER(identifier) WHERE type = ?",
@(SNTRuleTypeTeamID)];
// Note: Intentionally not attempting to migrate exsting SigningID rules to enforce
// the TeamID component to be uppercase. Since this is a newer rule type, it is
// assumed to be unnecessary and we'd rather not maintain the SQL to perform this
// migration automatically.
newVersion = 6;
}
if (version < 7) {
[db executeUpdate:@"ALTER TABLE 'rules' ADD 'customurl' TEXT"];
newVersion = 7;
}
// Save signing info for launchd and santad. Used to ensure they are always allowed.
self.santadCSInfo = [[MOLCodesignChecker alloc] initWithSelf];
self.launchdCSInfo = [[MOLCodesignChecker alloc] initWithPID:1];
@@ -230,20 +271,20 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
return count;
}
- (NSUInteger)binaryRuleCount {
- (NSUInteger)ruleCountForRuleType:(SNTRuleType)ruleType {
__block NSUInteger count = 0;
[self inDatabase:^(FMDatabase *db) {
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=1"];
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=?", @(ruleType)];
}];
return count;
}
- (NSUInteger)binaryRuleCount {
return [self ruleCountForRuleType:SNTRuleTypeBinary];
}
- (NSUInteger)certificateRuleCount {
__block NSUInteger count = 0;
[self inDatabase:^(FMDatabase *db) {
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=2"];
}];
return count;
return [self ruleCountForRuleType:SNTRuleTypeCertificate];
}
- (NSUInteger)compilerRuleCount {
@@ -265,22 +306,25 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
}
- (NSUInteger)teamIDRuleCount {
__block NSUInteger count = 0;
[self inDatabase:^(FMDatabase *db) {
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=3"];
}];
return count;
return [self ruleCountForRuleType:SNTRuleTypeTeamID];
}
- (NSUInteger)signingIDRuleCount {
return [self ruleCountForRuleType:SNTRuleTypeSigningID];
}
- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs {
return [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
state:[rs intForColumn:@"state"]
type:[rs intForColumn:@"type"]
customMsg:[rs stringForColumn:@"custommsg"]
timestamp:[rs intForColumn:@"timestamp"]];
SNTRule *r = [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
state:[rs intForColumn:@"state"]
type:[rs intForColumn:@"type"]
customMsg:[rs stringForColumn:@"custommsg"]
timestamp:[rs intForColumn:@"timestamp"]];
r.customURL = [rs stringForColumn:@"customurl"];
return r;
}
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
signingID:(NSString *)signingID
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID {
__block SNTRule *rule;
@@ -288,12 +332,27 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
// Look for a static rule that matches.
NSDictionary *staticRules = [[SNTConfigurator configurator] staticRules];
if (staticRules.count) {
// IMPORTANT: The order static rules are checked here should be the same
// order as given by the SQL query for the rules database.
rule = staticRules[binarySHA256];
if (rule.type == SNTRuleTypeBinary) return rule;
if (rule.type == SNTRuleTypeBinary) {
return rule;
}
rule = staticRules[signingID];
if (rule.type == SNTRuleTypeSigningID) {
return rule;
}
rule = staticRules[certificateSHA256];
if (rule.type == SNTRuleTypeCertificate) return rule;
if (rule.type == SNTRuleTypeCertificate) {
return rule;
}
rule = staticRules[teamID];
if (rule.type == SNTRuleTypeTeamID) return rule;
if (rule.type == SNTRuleTypeTeamID) {
return rule;
}
}
// Now query the database.
@@ -301,7 +360,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
// NOTE: This code is written with the intention that the binary rule is searched for first
// as Santa is designed to go with the most-specific rule possible.
//
// The intended order of precedence is Binaries > Certificates > Team IDs.
// The intended order of precedence is Binaries > Signing IDs > Certificates > Team IDs.
//
// As such the query should have "ORDER BY type DESC" before the LIMIT, to ensure that is the
// case. However, in all tested versions of SQLite that ORDER BY clause is unnecessary: the query
@@ -316,10 +375,12 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
// There is a test for this in SNTRuleTableTests in case SQLite behavior changes in the future.
//
[self inDatabase:^(FMDatabase *db) {
FMResultSet *rs =
[db executeQuery:@"SELECT * FROM rules WHERE (identifier=? and type=1) OR "
@"(identifier=? AND type=2) OR (identifier=? AND type=3) LIMIT 1",
binarySHA256, certificateSHA256, teamID];
FMResultSet *rs = [db executeQuery:@"SELECT * FROM rules WHERE "
@" (identifier=? and type=1000) "
@"OR (identifier=? AND type=2000) "
@"OR (identifier=? AND type=3000) "
@"OR (identifier=? AND type=4000) LIMIT 1",
binarySHA256, signingID, certificateSHA256, teamID];
if ([rs next]) {
rule = [self ruleFromResultSet:rs];
}
@@ -373,10 +434,10 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
}
} else {
if (![db executeUpdate:@"INSERT OR REPLACE INTO rules "
@"(identifier, state, type, custommsg, timestamp) "
@"VALUES (?, ?, ?, ?, ?);",
@"(identifier, state, type, custommsg, customurl, timestamp) "
@"VALUES (?, ?, ?, ?, ?, ?);",
rule.identifier, @(rule.state), @(rule.type), rule.customMsg,
@(rule.timestamp)]) {
rule.customURL, @(rule.timestamp)]) {
[self fillError:error
code:SNTRuleTableErrorInsertOrReplaceFailed
message:[db lastErrorMessage]];

View File

@@ -36,13 +36,26 @@
- (SNTRule *)_exampleTeamIDRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"teamID";
r.identifier = @"ABCDEFGHIJ";
r.state = SNTRuleStateBlock;
r.type = SNTRuleTypeTeamID;
r.customMsg = @"A teamID rule";
return r;
}
- (SNTRule *)_exampleSigningIDRuleIsPlatform:(BOOL)isPlatformBinary {
SNTRule *r = [[SNTRule alloc] init];
if (isPlatformBinary) {
r.identifier = @"platform:signingID";
} else {
r.identifier = @"ABCDEFGHIJ:signingID";
}
r.state = SNTRuleStateBlock;
r.type = SNTRuleTypeSigningID;
r.customMsg = @"A signingID rule";
return r;
}
- (SNTRule *)_exampleBinaryRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670";
@@ -127,6 +140,7 @@
SNTRule *r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
signingID:nil
certificateSHA256:nil
teamID:nil];
XCTAssertNotNil(r);
@@ -136,6 +150,7 @@
r = [self.sut
ruleForBinarySHA256:@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2"
signingID:nil
certificateSHA256:nil
teamID:nil];
XCTAssertNil(r);
@@ -148,6 +163,7 @@
SNTRule *r = [self.sut
ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:nil];
XCTAssertNotNil(r);
@@ -157,6 +173,7 @@
r = [self.sut
ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562"
teamID:nil];
XCTAssertNil(r);
@@ -167,28 +184,68 @@
cleanSlate:NO
error:nil];
SNTRule *r = [self.sut ruleForBinarySHA256:nil certificateSHA256:nil teamID:@"teamID"];
SNTRule *r = [self.sut ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:nil
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"teamID");
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(r.type, SNTRuleTypeTeamID);
XCTAssertEqual([self.sut teamIDRuleCount], 1);
r = [self.sut ruleForBinarySHA256:nil certificateSHA256:nil teamID:@"nonexistentTeamID"];
r = [self.sut ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:nil
teamID:@"nonexistentTeamID"];
XCTAssertNil(r);
}
- (void)testFetchSigningIDRule {
[self.sut addRules:@[
[self _exampleBinaryRule], [self _exampleSigningIDRuleIsPlatform:YES],
[self _exampleSigningIDRuleIsPlatform:NO]
]
cleanSlate:NO
error:nil];
XCTAssertEqual([self.sut signingIDRuleCount], 2);
SNTRule *r = [self.sut ruleForBinarySHA256:nil
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:nil
teamID:nil];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
r = [self.sut ruleForBinarySHA256:nil
signingID:@"platform:signingID"
certificateSHA256:nil
teamID:nil];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"platform:signingID");
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
r = [self.sut ruleForBinarySHA256:nil signingID:@"nonexistent" certificateSHA256:nil teamID:nil];
XCTAssertNil(r);
}
- (void)testFetchRuleOrdering {
[self.sut
addRules:@[ [self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule] ]
cleanSlate:NO
error:nil];
[self.sut addRules:@[
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
[self _exampleSigningIDRuleIsPlatform:NO]
]
cleanSlate:NO
error:nil];
// This test verifies that the implicit rule ordering we've been abusing is still working.
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
SNTRule *r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
@@ -196,8 +253,9 @@
r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"unknowncert"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
@@ -205,12 +263,29 @@
r = [self.sut
ruleForBinarySHA256:@"unknown"
signingID:@"unknown"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
r = [self.sut ruleForBinarySHA256:@"unknown"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"unknown"
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
XCTAssertEqual(r.type, SNTRuleTypeSigningID, @"Implicit rule ordering failed (SigningID)");
r = [self.sut ruleForBinarySHA256:@"unknown"
signingID:@"unknown"
certificateSHA256:@"unknown"
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(r.type, SNTRuleTypeTeamID, @"Implicit rule ordering failed (TeamID)");
}
- (void)testBadDatabase {

View File

@@ -33,6 +33,9 @@ static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType =
WatchItemPathType::kLiteral;
static constexpr bool kWatchItemPolicyDefaultAllowReadAccess = false;
static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
static constexpr bool kWatchItemPolicyDefaultInvertProcessExceptions = false;
static constexpr bool kWatchItemPolicyDefaultEnableSilentMode = false;
static constexpr bool kWatchItemPolicyDefaultEnableSilentTTYMode = false;
struct WatchItemPolicy {
struct Process {
@@ -69,19 +72,31 @@ struct WatchItemPolicy {
WatchItemPathType pt = kWatchItemPolicyDefaultPathType,
bool ara = kWatchItemPolicyDefaultAllowReadAccess,
bool ao = kWatchItemPolicyDefaultAuditOnly,
std::vector<Process> procs = {})
bool ipe = kWatchItemPolicyDefaultInvertProcessExceptions,
bool esm = kWatchItemPolicyDefaultEnableSilentMode,
bool estm = kWatchItemPolicyDefaultEnableSilentTTYMode,
std::string_view cm = "", std::vector<Process> procs = {})
: name(n),
path(p),
path_type(pt),
allow_read_access(ara),
audit_only(ao),
invert_process_exceptions(ipe),
silent(esm),
silent_tty(estm),
custom_message(cm.length() == 0 ? std::nullopt
: std::make_optional<std::string>(cm)),
processes(std::move(procs)) {}
bool operator==(const WatchItemPolicy &other) const {
// Note: Custom message isn't currently considered for equality purposes
return name == other.name && path == other.path &&
path_type == other.path_type &&
allow_read_access == other.allow_read_access &&
audit_only == other.audit_only && processes == other.processes;
audit_only == other.audit_only &&
invert_process_exceptions == other.invert_process_exceptions &&
silent == other.silent && silent_tty == other.silent_tty &&
processes == other.processes;
}
bool operator!=(const WatchItemPolicy &other) const {
@@ -93,7 +108,14 @@ struct WatchItemPolicy {
WatchItemPathType path_type;
bool allow_read_access;
bool audit_only;
bool invert_process_exceptions;
bool silent;
bool silent_tty;
std::optional<std::string> custom_message;
std::vector<Process> processes;
// WIP - No current way to control via config
std::string version = "temp_version";
};
} // namespace santa::santad::data_layer

View File

@@ -39,6 +39,10 @@ extern NSString *const kWatchItemConfigKeyPathsIsPrefix;
extern NSString *const kWatchItemConfigKeyOptions;
extern NSString *const kWatchItemConfigKeyOptionsAllowReadAccess;
extern NSString *const kWatchItemConfigKeyOptionsAuditOnly;
extern NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions;
extern NSString *const kWatchItemConfigKeyOptionsEnableSilentMode;
extern NSString *const kWatchItemConfigKeyOptionsEnableSilentTTYMode;
extern NSString *const kWatchItemConfigKeyOptionsCustomMessage;
extern NSString *const kWatchItemConfigKeyProcesses;
extern NSString *const kWatchItemConfigKeyProcessesBinaryPath;
extern NSString *const kWatchItemConfigKeyProcessesCertificateSha256;

View File

@@ -53,6 +53,10 @@ NSString *const kWatchItemConfigKeyPathsIsPrefix = @"IsPrefix";
NSString *const kWatchItemConfigKeyOptions = @"Options";
NSString *const kWatchItemConfigKeyOptionsAllowReadAccess = @"AllowReadAccess";
NSString *const kWatchItemConfigKeyOptionsAuditOnly = @"AuditOnly";
NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions = @"InvertProcessExceptions";
NSString *const kWatchItemConfigKeyOptionsEnableSilentMode = @"EnableSilentMode";
NSString *const kWatchItemConfigKeyOptionsEnableSilentTTYMode = @"EnableSilentTTYMode";
NSString *const kWatchItemConfigKeyOptionsCustomMessage = @"BlockMessage";
NSString *const kWatchItemConfigKeyProcesses = @"Processes";
NSString *const kWatchItemConfigKeyProcessesBinaryPath = @"BinaryPath";
NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha256";
@@ -72,6 +76,10 @@ static constexpr NSUInteger kMaxSigningIDLength = 512;
// churn rebuilding glob paths based on the state of the filesystem.
static constexpr uint64_t kMinReapplyConfigFrequencySecs = 15;
// Semi-arbitrary max custom message length. The goal is to protect against
// potential unbounded lengths, but no real reason this cannot be higher.
static constexpr NSUInteger kWatchItemConfigOptionCustomMessageMaxLength = 2048;
namespace santa::santad::data_layer {
// Type aliases
@@ -125,6 +133,10 @@ static std::vector<uint8_t> HexStringToBytes(NSString *str) {
return bytes;
}
static inline bool GetBoolValue(NSDictionary *options, NSString *key, bool default_value) {
return options[key] ? [options[key] boolValue] : default_value;
}
// Given a length, returns a ValidatorBlock that confirms the
// string is a valid hex string of the given length.
ValidatorBlock HexValidator(NSUInteger expected_length) {
@@ -376,6 +388,14 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
/// <false/>
/// <key>AuditOnly</key>
/// <false/>
/// <key>InvertProcessExceptions</key>
/// <false/>
/// <key>EnableSilentMode</key>
/// <true/>
/// <key>EnableSilentTTYMode</key>
/// <true/>
/// <key>BlockMessage</key>
/// <string>...</string>
/// </dict>
/// <key>Processes</key>
/// <array>
@@ -402,22 +422,38 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
NSDictionary *options = watch_item[kWatchItemConfigKeyOptions];
if (options) {
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAllowReadAccess, [NSNumber class],
err)) {
return false;
NSArray<NSString *> *boolOptions = @[
kWatchItemConfigKeyOptionsAllowReadAccess,
kWatchItemConfigKeyOptionsAuditOnly,
kWatchItemConfigKeyOptionsInvertProcessExceptions,
kWatchItemConfigKeyOptionsEnableSilentMode,
kWatchItemConfigKeyOptionsEnableSilentTTYMode,
];
for (NSString *key in boolOptions) {
if (!VerifyConfigKey(options, key, [NSNumber class], err)) {
return false;
}
}
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAuditOnly, [NSNumber class], err)) {
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsCustomMessage, [NSString class], err,
false,
LenRangeValidator(0, kWatchItemConfigOptionCustomMessageMaxLength))) {
return false;
}
}
bool allow_read_access = options[kWatchItemConfigKeyOptionsAllowReadAccess]
? [options[kWatchItemConfigKeyOptionsAllowReadAccess] boolValue]
: kWatchItemPolicyDefaultAllowReadAccess;
bool audit_only = options[kWatchItemConfigKeyOptionsAuditOnly]
? [options[kWatchItemConfigKeyOptionsAuditOnly] boolValue]
: kWatchItemPolicyDefaultAuditOnly;
bool allow_read_access = GetBoolValue(options, kWatchItemConfigKeyOptionsAllowReadAccess,
kWatchItemPolicyDefaultAllowReadAccess);
bool audit_only =
GetBoolValue(options, kWatchItemConfigKeyOptionsAuditOnly, kWatchItemPolicyDefaultAuditOnly);
bool invert_process_exceptions =
GetBoolValue(options, kWatchItemConfigKeyOptionsInvertProcessExceptions,
kWatchItemPolicyDefaultInvertProcessExceptions);
bool enable_silent_mode = GetBoolValue(options, kWatchItemConfigKeyOptionsEnableSilentMode,
kWatchItemPolicyDefaultEnableSilentMode);
bool enable_silent_tty_mode = GetBoolValue(options, kWatchItemConfigKeyOptionsEnableSilentTTYMode,
kWatchItemPolicyDefaultEnableSilentTTYMode);
std::variant<Unit, ProcessList> proc_list = VerifyConfigWatchItemProcesses(watch_item, err);
if (std::holds_alternative<Unit>(proc_list)) {
@@ -427,7 +463,10 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
for (const PathAndTypePair &path_type_pair : std::get<PathList>(path_list)) {
policies.push_back(std::make_shared<WatchItemPolicy>(
NSStringToUTF8StringView(name), path_type_pair.first, path_type_pair.second,
allow_read_access, audit_only, std::get<ProcessList>(proc_list)));
allow_read_access, audit_only, invert_process_exceptions, enable_silent_mode,
enable_silent_tty_mode,
NSStringToUTF8StringView(options[kWatchItemConfigKeyOptionsCustomMessage]),
std::get<ProcessList>(proc_list)));
}
return true;

View File

@@ -35,6 +35,7 @@
using santa::common::Unit;
using santa::santad::data_layer::kWatchItemPolicyDefaultAllowReadAccess;
using santa::santad::data_layer::kWatchItemPolicyDefaultAuditOnly;
using santa::santad::data_layer::kWatchItemPolicyDefaultInvertProcessExceptions;
using santa::santad::data_layer::kWatchItemPolicyDefaultPathType;
using santa::santad::data_layer::WatchItemPathType;
using santa::santad::data_layer::WatchItemPolicy;
@@ -92,10 +93,6 @@ static NSMutableDictionary *WrapWatchItemsConfig(NSDictionary *config) {
return [@{@"Version" : @(kVersion.data()), @"WatchItems" : [config mutableCopy]} mutableCopy];
}
static NSString *RepeatedString(NSString *str, NSUInteger len) {
return [@"" stringByPaddingToLength:len withString:str startingAtIndex:0];
}
@interface WatchItemsTest : XCTestCase
@property NSFileManager *fileMgr;
@property NSString *testDir;
@@ -756,26 +753,66 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
&err));
// Options keys must be valid types
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAllowReadAccess : @""}
},
policies, &err));
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAllowReadAccess : @(0)}
},
policies, &err));
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAuditOnly : @""}
},
policies, &err));
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAuditOnly : @(0)}
},
policies, &err));
{
// Check bool option keys
for (NSString *key in @[
kWatchItemConfigKeyOptionsAllowReadAccess,
kWatchItemConfigKeyOptionsAuditOnly,
kWatchItemConfigKeyOptionsInvertProcessExceptions,
kWatchItemConfigKeyOptionsEnableSilentMode,
kWatchItemConfigKeyOptionsEnableSilentTTYMode,
]) {
// Parse bool option with invliad type
XCTAssertFalse(ParseConfigSingleWatchItem(
@"",
@{kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{key : @""}},
policies, &err));
// Parse bool option with valid type
XCTAssertTrue(ParseConfigSingleWatchItem(
@"",
@{kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{key : @(0)}},
policies, &err));
}
// Check other option keys
// kWatchItemConfigKeyOptionsCustomMessage - Invalid type
XCTAssertFalse(ParseConfigSingleWatchItem(
@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsCustomMessage : @[]}
},
policies, &err));
// kWatchItemConfigKeyOptionsCustomMessage zero length
XCTAssertTrue(ParseConfigSingleWatchItem(
@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsCustomMessage : @""}
},
policies, &err));
// kWatchItemConfigKeyOptionsCustomMessage valid "normal" length
XCTAssertTrue(ParseConfigSingleWatchItem(
@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions :
@{kWatchItemConfigKeyOptionsCustomMessage : @"This is a custom message"}
},
policies, &err));
// kWatchItemConfigKeyOptionsCustomMessage Invalid "long" length
XCTAssertFalse(ParseConfigSingleWatchItem(
@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions :
@{kWatchItemConfigKeyOptionsCustomMessage : RepeatedString(@"A", 4096)}
},
policies, &err));
}
// If processes are specified, they must be valid format
// Note: Full tests in `testVerifyConfigWatchItemProcesses`
@@ -790,9 +827,11 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
XCTAssertTrue(
ParseConfigSingleWatchItem(@"rule", @{kWatchItemConfigKeyPaths : @[ @"a" ]}, policies, &err));
XCTAssertEqual(policies.size(), 1);
XCTAssertEqual(*policies[0].get(), WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
kWatchItemPolicyDefaultAllowReadAccess,
kWatchItemPolicyDefaultAuditOnly, {}));
XCTAssertEqual(
*policies[0].get(),
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
kWatchItemPolicyDefaultAllowReadAccess, kWatchItemPolicyDefaultAuditOnly,
kWatchItemPolicyDefaultInvertProcessExceptions, {}));
// Test multiple paths, options, and processes
policies.clear();
@@ -806,7 +845,11 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
@[ @"a", @{kWatchItemConfigKeyPathsPath : @"b", kWatchItemConfigKeyPathsIsPrefix : @(YES)} ],
kWatchItemConfigKeyOptions : @{
kWatchItemConfigKeyOptionsAllowReadAccess : @(YES),
kWatchItemConfigKeyOptionsAuditOnly : @(NO)
kWatchItemConfigKeyOptionsAuditOnly : @(NO),
kWatchItemConfigKeyOptionsInvertProcessExceptions : @(YES),
kWatchItemConfigKeyOptionsEnableSilentMode : @(YES),
kWatchItemConfigKeyOptionsEnableSilentMode : @(NO),
kWatchItemConfigKeyOptionsCustomMessage : @"",
},
kWatchItemConfigKeyProcesses : @[
@{kWatchItemConfigKeyProcessesBinaryPath : @"pa"},
@@ -814,11 +857,12 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
]
},
policies, &err));
XCTAssertEqual(policies.size(), 2);
XCTAssertEqual(*policies[0].get(),
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType, true, false, procs));
XCTAssertEqual(*policies[1].get(),
WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true, false, procs));
XCTAssertEqual(*policies[0].get(), WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
true, false, true, true, false, "", procs));
XCTAssertEqual(*policies[1].get(), WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true,
false, true, true, false, "", procs));
}
- (void)testState {

View File

@@ -60,7 +60,8 @@ NSString *const FlushCacheReasonToString(FlushCacheReason reason) {
case FlushCacheReason::kExplicitCommand: return kFlushCacheReasonExplicitCommand;
case FlushCacheReason::kFilesystemUnmounted: return kFlushCacheReasonFilesystemUnmounted;
default:
[NSException raise:@"Invalid reason" format:@"Unknown reason value: %d", reason];
[NSException raise:@"Invalid reason"
format:@"Unknown reason value: %d", static_cast<int>(reason)];
return nil;
}
}

View File

@@ -16,6 +16,8 @@
#import <XCTest/XCTest.h>
#include <dispatch/dispatch.h>
#include <utility>
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
using santa::santad::event_providers::endpoint_security::Client;

View File

@@ -46,7 +46,11 @@ class EnrichedFile {
group_(std::move(other.group_)),
hash_(std::move(other.hash_)) {}
// Note: Move assignment could be safely implemented but not currently needed
EnrichedFile &operator=(EnrichedFile &&other) = delete;
EnrichedFile(const EnrichedFile &other) = delete;
EnrichedFile &operator=(const EnrichedFile &other) = delete;
const std::optional<std::shared_ptr<std::string>> &user() const {
return user_;
@@ -87,7 +91,11 @@ class EnrichedProcess {
real_group_(std::move(other.real_group_)),
executable_(std::move(other.executable_)) {}
// Note: Move assignment could be safely implemented but not currently needed
EnrichedProcess &operator=(EnrichedProcess &&other) = delete;
EnrichedProcess(const EnrichedProcess &other) = delete;
EnrichedProcess &operator=(const EnrichedProcess &other) = delete;
const std::optional<std::shared_ptr<std::string>> &effective_user() const {
return effective_user_;
@@ -123,7 +131,12 @@ class EnrichedEventType {
instigator_(std::move(other.instigator_)),
enrichment_time_(std::move(other.enrichment_time_)) {}
// Note: Move assignment could be safely implemented but not currently needed
// so no sense in implementing across all child classes
EnrichedEventType &operator=(EnrichedEventType &&other) = delete;
EnrichedEventType(const EnrichedEventType &other) = delete;
EnrichedEventType &operator=(const EnrichedEventType &other) = delete;
virtual ~EnrichedEventType() = default;

View File

@@ -34,7 +34,7 @@ class Enricher {
public:
Enricher();
virtual ~Enricher() = default;
virtual std::shared_ptr<EnrichedMessage> Enrich(Message &&msg);
virtual std::unique_ptr<EnrichedMessage> Enrich(Message &&msg);
virtual EnrichedProcess Enrich(
const es_process_t &es_proc,
EnrichOptions options = EnrichOptions::kDefault);

View File

@@ -30,19 +30,19 @@ namespace santa::santad::event_providers::endpoint_security {
Enricher::Enricher() : username_cache_(256), groupname_cache_(256) {}
std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
std::unique_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
// TODO(mlw): Consider potential design patterns that could help reduce memory usage under load
// (such as maybe the flyweight pattern)
switch (es_msg->event_type) {
case ES_EVENT_TYPE_NOTIFY_CLOSE:
return std::make_shared<EnrichedMessage>(EnrichedClose(
return std::make_unique<EnrichedMessage>(EnrichedClose(
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.close.target)));
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA:
return std::make_shared<EnrichedMessage>(EnrichedExchange(
return std::make_unique<EnrichedMessage>(EnrichedExchange(
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exchangedata.file1),
Enrich(*es_msg->event.exchangedata.file2)));
case ES_EVENT_TYPE_NOTIFY_EXEC:
return std::make_shared<EnrichedMessage>(EnrichedExec(
return std::make_unique<EnrichedMessage>(EnrichedExec(
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exec.target),
(es_msg->version >= 2 && es_msg->event.exec.script)
? std::make_optional(Enrich(*es_msg->event.exec.script))
@@ -51,28 +51,28 @@ std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
? std::make_optional(Enrich(*es_msg->event.exec.cwd))
: std::nullopt));
case ES_EVENT_TYPE_NOTIFY_FORK:
return std::make_shared<EnrichedMessage>(EnrichedFork(
return std::make_unique<EnrichedMessage>(EnrichedFork(
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.fork.child)));
case ES_EVENT_TYPE_NOTIFY_EXIT:
return std::make_shared<EnrichedMessage>(
return std::make_unique<EnrichedMessage>(
EnrichedExit(std::move(es_msg), Enrich(*es_msg->process)));
case ES_EVENT_TYPE_NOTIFY_LINK:
return std::make_shared<EnrichedMessage>(
return std::make_unique<EnrichedMessage>(
EnrichedLink(std::move(es_msg), Enrich(*es_msg->process),
Enrich(*es_msg->event.link.source), Enrich(*es_msg->event.link.target_dir)));
case ES_EVENT_TYPE_NOTIFY_RENAME: {
if (es_msg->event.rename.destination_type == ES_DESTINATION_TYPE_NEW_PATH) {
return std::make_shared<EnrichedMessage>(EnrichedRename(
return std::make_unique<EnrichedMessage>(EnrichedRename(
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
std::nullopt, Enrich(*es_msg->event.rename.destination.new_path.dir)));
} else {
return std::make_shared<EnrichedMessage>(EnrichedRename(
return std::make_unique<EnrichedMessage>(EnrichedRename(
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
Enrich(*es_msg->event.rename.destination.existing_file), std::nullopt));
}
}
case ES_EVENT_TYPE_NOTIFY_UNLINK:
return std::make_shared<EnrichedMessage>(EnrichedUnlink(
return std::make_unique<EnrichedMessage>(EnrichedUnlink(
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.unlink.target)));
default:
// This is a programming error

View File

@@ -193,13 +193,12 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
return _esApi->UnsubscribeAll(_esClient);
}
- (bool)unmuteEverything {
bool result = _esApi->UnmuteAllPaths(_esClient);
result = _esApi->UnmuteAllTargetPaths(_esClient) && result;
return result;
- (bool)unmuteAllTargetPaths {
return _esApi->UnmuteAllTargetPaths(_esClient);
}
- (bool)enableTargetPathWatching {
[self unmuteAllTargetPaths];
return _esApi->InvertTargetPathMuting(_esClient);
}
@@ -236,9 +235,9 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
}
}
- (void)processEnrichedMessage:(std::shared_ptr<EnrichedMessage>)msg
handler:(void (^)(std::shared_ptr<EnrichedMessage>))messageHandler {
__block std::shared_ptr<EnrichedMessage> msgTmp = std::move(msg);
- (void)processEnrichedMessage:(std::unique_ptr<EnrichedMessage>)msg
handler:(void (^)(std::unique_ptr<EnrichedMessage>))messageHandler {
__block std::unique_ptr<EnrichedMessage> msgTmp = std::move(msg);
dispatch_async(_notifyQueue, ^{
messageHandler(std::move(msgTmp));
});

View File

@@ -49,7 +49,7 @@
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events;
- (bool)unsubscribeAll;
- (bool)unmuteEverything;
- (bool)unmuteAllTargetPaths;
- (bool)enableTargetPathWatching;
- (bool)muteTargetPaths:
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>> &)paths;
@@ -72,9 +72,9 @@
- (void)
processEnrichedMessage:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage>)msg
(std::unique_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage>)msg
handler:
(void (^)(std::shared_ptr<
(void (^)(std::unique_ptr<
santa::santad::event_providers::endpoint_security::EnrichedMessage>))
messageHandler;

View File

@@ -274,23 +274,20 @@ using santa::santad::event_providers::endpoint_security::Message;
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testUnmuteEverything {
- (void)testUnmuteAllTargetPaths {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
// Test variations of underlying unmute impls returning both true and false
EXPECT_CALL(*mockESApi, UnmuteAllPaths)
.WillOnce(testing::Return(true))
.WillOnce(testing::Return(false));
// Test the underlying unmute impl returning both true and false
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths)
.WillOnce(testing::Return(true))
.WillOnce(testing::Return(true));
.WillOnce(testing::Return(false));
XCTAssertTrue([client unmuteEverything]);
XCTAssertFalse([client unmuteEverything]);
XCTAssertTrue([client unmuteAllTargetPaths]);
XCTAssertFalse([client unmuteAllTargetPaths]);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
@@ -302,6 +299,9 @@ using santa::santad::event_providers::endpoint_security::Message;
metrics:nullptr
processor:Processor::kUnknown];
// UnmuteAllTargetPaths is always attempted.
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).Times(2).WillRepeatedly(testing::Return(true));
// Test the underlying invert nute impl returning both true and false
EXPECT_CALL(*mockESApi, InvertTargetPathMuting)
.WillOnce(testing::Return(true))
@@ -406,14 +406,14 @@ using santa::santad::event_providers::endpoint_security::Message;
metrics:nullptr
processor:Processor::kUnknown];
{
auto enrichedMsg = std::make_shared<EnrichedMessage>(
auto enrichedMsg = std::make_unique<EnrichedMessage>(
EnrichedClose(Message(mockESApi, &esMsg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
[client processEnrichedMessage:enrichedMsg
handler:^(std::shared_ptr<EnrichedMessage> msg) {
[client processEnrichedMessage:std::move(enrichedMsg)
handler:^(std::unique_ptr<EnrichedMessage> msg) {
// reset the shared_ptr to drop the held message.
// This is a workaround for a TSAN only false positive
// which happens if we switch back to the sem wait

View File

@@ -16,6 +16,7 @@
#include <memory>
#import "Source/common/SNTFileAccessEvent.h"
#include "Source/santad/DataLayer/WatchItems.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
@@ -24,6 +25,9 @@
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/santad/Metrics.h"
#import "Source/santad/SNTDecisionCache.h"
#include "Source/santad/TTYWriter.h"
typedef void (^SNTFileAccessBlockCallback)(SNTFileAccessEvent *event);
@interface SNTEndpointSecurityFileAccessAuthorizer
: SNTEndpointSecurityClient <SNTEndpointSecurityDynamicEventHandler>
@@ -36,6 +40,9 @@
watchItems:(std::shared_ptr<santa::santad::data_layer::WatchItems>)watchItems
enricher:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
decisionCache:(SNTDecisionCache *)decisionCache;
decisionCache:(SNTDecisionCache *)decisionCache
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter;
@property SNTFileAccessBlockCallback fileAccessBlockCallback;
@end

View File

@@ -18,32 +18,40 @@
#include <Kernel/kern/cs_blobs.h>
#import <MOLCertificate/MOLCertificate.h>
#import <MOLCodesignChecker/MOLCodesignChecker.h>
#include <bsm/libbsm.h>
#include <sys/fcntl.h>
#include <sys/types.h>
#include <algorithm>
#include <array>
#include <cstdlib>
#include <memory>
#include <optional>
#include <set>
#include <type_traits>
#include <utility>
#include <variant>
#include "Source/common/Platform.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#include "Source/common/SNTFileAccessEvent.h"
#import "Source/common/SNTMetricSet.h"
#import "Source/common/SNTStrengthify.h"
#include "Source/common/SantaCache.h"
#include "Source/common/SantaVnode.h"
#include "Source/common/SantaVnodeHash.h"
#include "Source/common/String.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"
#include "Source/santad/DataLayer/WatchItems.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/RateLimiter.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
using santa::common::StringToNSString;
using santa::santad::EventDisposition;
using santa::santad::TTYWriter;
using santa::santad::data_layer::WatchItemPathType;
using santa::santad::data_layer::WatchItemPolicy;
using santa::santad::data_layer::WatchItems;
@@ -65,6 +73,106 @@ static constexpr uint16_t kDefaultRateLimitQPS = 50;
struct PathTarget {
std::string path;
bool isReadable;
std::optional<std::pair<dev_t, ino_t>> devnoIno;
};
// This is a bespoke cache for mapping processes to a set of files the process
// has previously been allowed to read as defined by policy. It has similar
// semantics to SantaCache in terms of clearing the cache keys and values when
// max sizes are reached.
// TODO: We need a proper LRU cache
//
// NB: SantaCache should not be used here.
// 1.) It doesn't efficiently support non-primitive value types. Since the
// value of each key needs to be a set, we want to refrain from having to
// unnecessarily copy the value.
// 2.) It doesn't support size limits on value types
class ProcessFiles {
using FileSet = absl::flat_hash_set<std::pair<dev_t, ino_t>>;
public:
ProcessFiles() {
q_ = dispatch_queue_create(
"com.google.santa.daemon.faa",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
QOS_CLASS_USER_INTERACTIVE, 0));
};
// Add the given target to the set of files a process can read
void Set(const es_process_t *proc, const PathTarget &target) {
if (!target.devnoIno.has_value()) {
return;
}
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
audit_token_to_pidversion(proc->audit_token)};
dispatch_sync(q_, ^{
// If we hit the size limit, clear the cache to prevent unbounded growth
if (cache_.size() >= kMaxCacheSize) {
ClearLocked();
}
FileSet &fs = cache_[std::move(pidPidver)];
// If we hit the per-entry size limit, clear the entry to prevent unbounded growth
if (fs.size() >= kMaxCacheEntrySize) {
fs.clear();
}
fs.insert(*target.devnoIno);
});
}
// Remove the given process from the cache
void Remove(const es_process_t *proc) {
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
audit_token_to_pidversion(proc->audit_token)};
dispatch_sync(q_, ^{
cache_.erase(pidPidver);
});
}
// Check if the set of files for a given process contains the given file
bool Exists(const es_process_t *proc, const es_file_t *file) {
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
audit_token_to_pidversion(proc->audit_token)};
std::pair<dev_t, ino_t> devnoIno = {file->stat.st_dev, file->stat.st_ino};
__block bool exists = false;
dispatch_sync(q_, ^{
const auto &iter = cache_.find(pidPidver);
if (iter != cache_.end() && iter->second.count(devnoIno) > 0) {
exists = true;
}
});
return exists;
}
// Clear all cache entries
void Clear() {
dispatch_sync(q_, ^{
ClearLocked();
});
}
private:
// Remove everything in the cache.
void ClearLocked() { cache_.clear(); }
dispatch_queue_t q_;
absl::flat_hash_map<std::pair<pid_t, pid_t>, FileSet> cache_;
// Cache limits are merely meant to protect against unbounded growth. In practice,
// the observed cache size is typically small for normal WatchItems rules (those
// that do not target high-volume paths). The per entry size was observed to vary
// quite dramatically based on the type of process (e.g. large, complex applications
// were observed to frequently have several thousands of entries).
static constexpr size_t kMaxCacheSize = 512;
static constexpr size_t kMaxCacheEntrySize = 8192;
};
static inline std::string Path(const es_file_t *esFile) {
@@ -78,14 +186,18 @@ static inline std::string Path(const es_string_token_t &tok) {
static inline void PushBackIfNotTruncated(std::vector<PathTarget> &vec, const es_file_t *esFile,
bool isReadable = false) {
if (!esFile->path_truncated) {
vec.push_back({Path(esFile), isReadable});
vec.push_back({Path(esFile), isReadable,
isReadable ? std::make_optional<std::pair<dev_t, ino_t>>(
{esFile->stat.st_dev, esFile->stat.st_ino})
: std::nullopt});
}
}
// Note: This variant of PushBackIfNotTruncated can never be marked "isReadable"
static inline void PushBackIfNotTruncated(std::vector<PathTarget> &vec, const es_file_t *dir,
const es_string_token_t &name, bool isReadable = false) {
const es_string_token_t &name) {
if (!dir->path_truncated) {
vec.push_back({Path(dir) + "/" + Path(name), isReadable});
vec.push_back({Path(dir) + "/" + Path(name), false, std::nullopt});
}
}
@@ -99,9 +211,9 @@ es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyDecision
case FileAccessPolicyDecision::kAllowedAuditOnly: return ES_AUTH_RESULT_ALLOW;
default:
// This is a programming error. Bail.
LOGE(@"Invalid file access decision encountered: %d", decision);
LOGE(@"Invalid file access decision encountered: %d", static_cast<int>(decision));
[NSException raise:@"Invalid FileAccessPolicyDecision"
format:@"Invalid FileAccessPolicyDecision: %d", decision];
format:@"Invalid FileAccessPolicyDecision: %d", static_cast<int>(decision)];
}
}
@@ -114,6 +226,10 @@ bool ShouldLogDecision(FileAccessPolicyDecision decision) {
}
}
bool ShouldNotifyUserDecision(FileAccessPolicyDecision decision) {
return ShouldLogDecision(decision) && decision != FileAccessPolicyDecision::kAllowedAuditOnly;
}
es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_result_t result2) {
// If either policy denied the operation, the operation is denied
return ((result1 == ES_AUTH_RESULT_DENY || result2 == ES_AUTH_RESULT_DENY)
@@ -203,6 +319,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
std::shared_ptr<Enricher> _enricher;
std::shared_ptr<RateLimiter> _rateLimiter;
SantaCache<SantaVnode, NSString *> _certHashCache;
std::shared_ptr<TTYWriter> _ttyWriter;
ProcessFiles _readsCache;
}
- (instancetype)
@@ -213,7 +331,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
watchItems:(std::shared_ptr<WatchItems>)watchItems
enricher:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
decisionCache:(SNTDecisionCache *)decisionCache {
decisionCache:(SNTDecisionCache *)decisionCache
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter {
self = [super initWithESAPI:std::move(esApi)
metrics:metrics
processor:santa::santad::Processor::kFileAccessAuthorizer];
@@ -221,8 +340,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
_watchItems = std::move(watchItems);
_logger = std::move(logger);
_enricher = std::move(enricher);
_decisionCache = decisionCache;
_ttyWriter = std::move(ttyWriter);
_rateLimiter = RateLimiter::Create(metrics, santa::santad::Processor::kFileAccessAuthorizer,
kDefaultRateLimitQPS);
@@ -241,7 +360,6 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
[self establishClientOrDie];
[super enableTargetPathWatching];
[super unmuteEverything];
}
return self;
}
@@ -417,6 +535,11 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
std::shared_ptr<WatchItemPolicy> policy = optionalPolicy.value();
// If policy allows reading, add target to the cache
if (policy->allow_read_access && target.isReadable) {
self->_readsCache.Set(msg->process, target);
}
// Check if this action contains any special case that would produce
// an immediate result.
FileAccessPolicyDecision specialCase = [self specialCaseForPolicy:policy
@@ -426,19 +549,35 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
return specialCase;
}
FileAccessPolicyDecision decision = FileAccessPolicyDecision::kDenied;
for (const WatchItemPolicy::Process &process : policy->processes) {
if ([self policyProcess:process matchesESProcess:msg->process]) {
return FileAccessPolicyDecision::kAllowed;
decision = FileAccessPolicyDecision::kAllowed;
break;
}
}
if (policy->audit_only) {
return FileAccessPolicyDecision::kAllowedAuditOnly;
} else {
// TODO(xyz): Write to TTY like in exec controller?
// TODO(xyz): Need new config item for custom message in UI
return FileAccessPolicyDecision::kDenied;
// If the `invert_process_exceptions` option is set, the decision should be
// inverted from allowed to denied or vice versa. Note that this inversion
// must be made prior to checking the policy's audit-only flag.
if (policy->invert_process_exceptions) {
if (decision == FileAccessPolicyDecision::kAllowed) {
decision = FileAccessPolicyDecision::kDenied;
} else {
decision = FileAccessPolicyDecision::kAllowed;
}
}
if (decision == FileAccessPolicyDecision::kDenied && policy->audit_only) {
decision = FileAccessPolicyDecision::kAllowedAuditOnly;
}
// https://github.com/google/santa/issues/1084
// TODO(xyz): Write to TTY like in exec controller?
// TODO(xyz): Need new config item for custom message in UI
return decision;
}
- (FileAccessPolicyDecision)handleMessage:(const Message &)msg
@@ -466,6 +605,28 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
targetPathCopy, policyDecision);
}];
}
#if 0
if (!optionalPolicy.value()->silent && self.fileAccessBlockCallback) {
SNTCachedDecision *cd =
[self.decisionCache cachedDecisionForFile:msg->process->executable->stat];
SNTFileAccessEvent *event = [[SNTFileAccessEvent alloc] init];
event.accessedPath = StringToNSString(target.path);
event.ruleVersion = StringToNSString(optionalPolicy.value()->version);
event.ruleName = StringToNSString(optionalPolicy.value()->name);
event.fileSHA256 = cd.sha256 ?: @"<unknown sha>";
event.filePath = StringToNSString(msg->process->executable->path.data);
event.teamID = cd.teamID ?: @"<unknown team id>";
event.teamID = cd.signingID ?: @"<unknown signing id>";
event.pid = @(audit_token_to_pid(msg->process->audit_token));
event.ppid = @(audit_token_to_pid(msg->process->parent_audit_token));
event.parentName = StringToNSString(msg.ParentProcessName());
self.fileAccessBlockCallback(event);
}
#endif
}
return policyDecision;
@@ -518,6 +679,18 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
- (void)handleMessage:(santa::santad::event_providers::endpoint_security::Message &&)esMsg
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
if (esMsg->event_type == ES_EVENT_TYPE_AUTH_OPEN &&
!(esMsg->event.open.fflag & kOpenFlagsIndicatingWrite)) {
if (self->_readsCache.Exists(esMsg->process, esMsg->event.open.file)) {
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:false];
return;
}
} else if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_EXIT) {
// On process exit, remove the cache entry
self->_readsCache.Remove(esMsg->process);
return;
}
[self processMessage:std::move(esMsg)
handler:^(const Message &msg) {
[self processMessage:msg];
@@ -526,11 +699,10 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
}
- (void)enable {
// TODO(xyz): Expand to support ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_TRUNCATE
std::set<es_event_type_t> events = {
ES_EVENT_TYPE_AUTH_CLONE, ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_EXCHANGEDATA,
ES_EVENT_TYPE_AUTH_LINK, ES_EVENT_TYPE_AUTH_OPEN, ES_EVENT_TYPE_AUTH_RENAME,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK, ES_EVENT_TYPE_NOTIFY_EXIT,
};
#if HAVE_MACOS_12
@@ -554,7 +726,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
if ([super unsubscribeAll]) {
self.isSubscribed = false;
}
[super unmuteEverything];
[super unmuteAllTargetPaths];
}
}
@@ -574,6 +746,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
// begin receiving events (if not already)
[self enable];
}
self->_readsCache.Clear();
}
@end

View File

@@ -20,6 +20,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <sys/fcntl.h>
#include <sys/types.h>
#include <cstring>
#include <array>
@@ -50,18 +51,23 @@ extern NSString *kBadCertHash;
struct PathTarget {
std::string path;
bool isReadable;
std::optional<std::pair<dev_t, ino_t>> devnoIno;
};
using PathTargetsPair = std::pair<std::optional<std::string>, std::optional<std::string>>;
extern void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets);
extern es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyDecision decision);
extern bool ShouldLogDecision(FileAccessPolicyDecision decision);
extern bool ShouldNotifyUserDecision(FileAccessPolicyDecision decision);
extern es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_result_t result2);
static inline std::pair<dev_t, ino_t> FileID(const es_file_t &file) {
return std::make_pair(file.stat.st_dev, file.stat.st_ino);
}
void SetExpectationsForFileAccessAuthorizerInit(
std::shared_ptr<MockEndpointSecurityAPI> mockESApi) {
EXPECT_CALL(*mockESApi, InvertTargetPathMuting).WillOnce(testing::Return(true));
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
}
@@ -136,7 +142,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:self.dcMock];
decisionCache:self.dcMock
ttyWriter:nullptr];
//
// Test 1 - Not in local cache or decision cache, and code sig lookup fails
@@ -230,7 +237,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
{FileAccessPolicyDecision::kAllowed, false},
{FileAccessPolicyDecision::kAllowedReadAccess, false},
{FileAccessPolicyDecision::kAllowedAuditOnly, true},
{(FileAccessPolicyDecision)5, false},
{(FileAccessPolicyDecision)123, false},
};
for (const auto &kv : policyDecisionToShouldLog) {
@@ -238,6 +245,22 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
}
}
- (void)testShouldNotifyUserDecision {
std::map<FileAccessPolicyDecision, bool> policyDecisionToShouldLog = {
{FileAccessPolicyDecision::kNoPolicy, false},
{FileAccessPolicyDecision::kDenied, true},
{FileAccessPolicyDecision::kDeniedInvalidSignature, true},
{FileAccessPolicyDecision::kAllowed, false},
{FileAccessPolicyDecision::kAllowedReadAccess, false},
{FileAccessPolicyDecision::kAllowedAuditOnly, false},
{(FileAccessPolicyDecision)123, false},
};
for (const auto &kv : policyDecisionToShouldLog) {
XCTAssertEqual(ShouldNotifyUserDecision(kv.first), kv.second);
}
}
- (void)testCombinePolicyResults {
// Ensure that the combined result is ES_AUTH_RESULT_DENY if both or either
// input result is ES_AUTH_RESULT_DENY.
@@ -270,7 +293,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
auto policy = std::make_shared<WatchItemPolicy>("foo_policy", "/foo");
@@ -398,7 +422,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
id accessClientMock = OCMPartialMock(accessClient);
@@ -516,7 +541,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
id accessClientMock = OCMPartialMock(accessClient);
@@ -532,8 +558,9 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
// If no policy exists, the operation is allowed
{
Message msg(mockESApi, &esMsg);
XCTAssertEqual([accessClient applyPolicy:std::nullopt forTarget:target toMessage:msg],
XCTAssertEqual([accessClient applyPolicy:std::nullopt
forTarget:target
toMessage:Message(mockESApi, &esMsg)],
FileAccessPolicyDecision::kNoPolicy);
}
@@ -546,8 +573,9 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
{
OCMExpect([self.mockConfigurator enableBadSignatureProtection]).andReturn(YES);
esMsg.process->codesigning_flags = CS_SIGNED;
Message msg(mockESApi, &esMsg);
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
forTarget:target
toMessage:Message(mockESApi, &esMsg)],
FileAccessPolicyDecision::kDeniedInvalidSignature);
}
@@ -557,11 +585,12 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
{
OCMExpect([self.mockConfigurator enableBadSignatureProtection]).andReturn(NO);
esMsg.process->codesigning_flags = CS_SIGNED;
Message msg(mockESApi, &esMsg);
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
.ignoringNonObjectArgs()
.andReturn(true);
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
forTarget:target
toMessage:Message(mockESApi, &esMsg)],
FileAccessPolicyDecision::kAllowed);
}
@@ -574,8 +603,9 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
.ignoringNonObjectArgs()
.andReturn(false);
policy->audit_only = false;
Message msg(mockESApi, &esMsg);
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
forTarget:target
toMessage:Message(mockESApi, &esMsg)],
FileAccessPolicyDecision::kDenied);
}
@@ -585,8 +615,50 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
.ignoringNonObjectArgs()
.andReturn(false);
policy->audit_only = true;
Message msg(mockESApi, &esMsg);
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
forTarget:target
toMessage:Message(mockESApi, &esMsg)],
FileAccessPolicyDecision::kAllowedAuditOnly);
}
// The remainder of the tests set the policy's `invert_process_exceptions` option
policy->invert_process_exceptions = true;
// If no exceptions for inverted policy, operations are allowed
{
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
.ignoringNonObjectArgs()
.andReturn(false);
policy->audit_only = false;
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
forTarget:target
toMessage:Message(mockESApi, &esMsg)],
FileAccessPolicyDecision::kAllowed);
}
// For audit only policies with no exception matches and inverted exceptions, operations are
// allowed
{
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
.ignoringNonObjectArgs()
.andReturn(false);
policy->audit_only = true;
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
forTarget:target
toMessage:Message(mockESApi, &esMsg)],
FileAccessPolicyDecision::kAllowed);
}
// For audit only policies with exception match and inverted exceptions, operations are allowed
// audit only
{
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
.ignoringNonObjectArgs()
.andReturn(true);
policy->audit_only = true;
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
forTarget:target
toMessage:Message(mockESApi, &esMsg)],
FileAccessPolicyDecision::kAllowedAuditOnly);
}
@@ -597,7 +669,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
std::set<es_event_type_t> expectedEventSubs = {
ES_EVENT_TYPE_AUTH_CLONE, ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_EXCHANGEDATA,
ES_EVENT_TYPE_AUTH_LINK, ES_EVENT_TYPE_AUTH_OPEN, ES_EVENT_TYPE_AUTH_RENAME,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK, ES_EVENT_TYPE_NOTIFY_EXIT,
};
#if HAVE_MACOS_12
@@ -633,10 +705,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
EXPECT_CALL(*mockESApi, UnsubscribeAll);
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
accessClient.isSubscribed = true;
@@ -648,9 +720,9 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
- (void)testGetPathTargets {
// This test ensures that the `GetPathTargets` functions returns the
// expected combination of targets for each handled event variant
es_file_t testFile1 = MakeESFile("test_file_1");
es_file_t testFile2 = MakeESFile("test_file_2");
es_file_t testDir = MakeESFile("test_dir");
es_file_t testFile1 = MakeESFile("test_file_1", MakeStat(100));
es_file_t testFile2 = MakeESFile("test_file_2", MakeStat(200));
es_file_t testDir = MakeESFile("test_dir", MakeStat(300));
es_string_token_t testTok = MakeESStringToken("test_tok");
std::string dirTok = std::string(testDir.path.data) + "/" + std::string(testTok.data);
@@ -671,6 +743,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
}
{
@@ -685,8 +758,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -703,8 +778,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -718,8 +795,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
}
@@ -733,6 +812,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
}
{
@@ -747,8 +827,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -762,8 +844,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -778,6 +862,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCppStringEqual(targets[0].path, dirTok);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
}
{
@@ -790,6 +875,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
}
if (@available(macOS 12.0, *)) {
@@ -808,8 +894,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -821,8 +909,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
}
}

View File

@@ -16,7 +16,9 @@
#include <EndpointSecurity/EndpointSecurity.h>
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#include "Source/common/String.h"
#include "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
@@ -44,6 +46,7 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
@interface SNTEndpointSecurityRecorder ()
@property SNTCompilerController *compilerController;
@property SNTConfigurator *configurator;
@end
@implementation SNTEndpointSecurityRecorder {
@@ -69,6 +72,7 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
_compilerController = compilerController;
_authResultCache = authResultCache;
_prefixTree = prefixTree;
_configurator = [SNTConfigurator configurator];
[self establishClientOrDie];
}
@@ -83,7 +87,7 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
// Pre-enrichment processing
switch (esMsg->event_type) {
case ES_EVENT_TYPE_NOTIFY_CLOSE:
case ES_EVENT_TYPE_NOTIFY_CLOSE: {
// TODO(mlw): Once we move to building with the macOS 13 SDK, we should also check
// the `was_mapped_writable` field
if (esMsg->event.close.modified == false) {
@@ -95,7 +99,23 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
}
self->_authResultCache->RemoveFromCache(esMsg->event.close.target);
// Only log file changes that match the given regex
NSString *targetPath = santa::common::StringToNSString(esMsg->event.close.target->path.data);
if (![[self.configurator fileChangesRegex]
numberOfMatchesInString:targetPath
options:0
range:NSMakeRange(0, targetPath.length)]) {
// Note: Do not record metrics in this case. These are not considered "drops"
// because this is not a failure case.
// TODO(mlw): Consider changes to configuration that would allow muting paths
// to filter on the kernel side rather than in user space.
return;
}
break;
}
default: break;
}
@@ -110,11 +130,11 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
// Enrich the message inline with the ES handler block to capture enrichment
// data as close to the source event as possible.
std::shared_ptr<EnrichedMessage> sharedEnrichedMessage = _enricher->Enrich(std::move(esMsg));
std::unique_ptr<EnrichedMessage> enrichedMessage = _enricher->Enrich(std::move(esMsg));
// Asynchronously log the message
[self processEnrichedMessage:std::move(sharedEnrichedMessage)
handler:^(std::shared_ptr<EnrichedMessage> msg) {
[self processEnrichedMessage:std::move(enrichedMessage)
handler:^(std::unique_ptr<EnrichedMessage> msg) {
self->_logger->Log(std::move(msg));
recordEventMetrics(EventDisposition::kProcessed);
}];

View File

@@ -23,6 +23,7 @@
#include <set>
#include "Source/common/PrefixTree.h"
#import "Source/common/SNTConfigurator.h"
#include "Source/common/TestUtils.h"
#include "Source/common/Unit.h"
#import "Source/santad/EventProviders/AuthResultCache.h"
@@ -48,7 +49,7 @@ using santa::santad::logs::endpoint_security::Logger;
class MockEnricher : public Enricher {
public:
MOCK_METHOD(std::shared_ptr<EnrichedMessage>, Enrich, (Message &&));
MOCK_METHOD(std::unique_ptr<EnrichedMessage>, Enrich, (Message &&));
};
class MockAuthResultCache : public AuthResultCache {
@@ -62,14 +63,25 @@ class MockLogger : public Logger {
public:
using Logger::Logger;
MOCK_METHOD(void, Log, (std::shared_ptr<EnrichedMessage>));
MOCK_METHOD(void, Log, (std::unique_ptr<EnrichedMessage>));
};
@interface SNTEndpointSecurityRecorderTest : XCTestCase
@property id mockConfigurator;
@end
@implementation SNTEndpointSecurityRecorderTest
- (void)setUp {
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
NSString *testPattern = @"^/foo/match.*";
NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:testPattern
options:0
error:NULL];
OCMStub([self.mockConfigurator fileChangesRegex]).andReturn(re);
}
- (void)testEnable {
// Ensure the client subscribes to expected event types
std::set<es_event_type_t> expectedEventSubs{
@@ -94,19 +106,21 @@ class MockLogger : public Logger {
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth);
es_file_t targetFile = MakeESFile("bar");
es_file_t targetFileMatchesRegex = MakeESFile("/foo/matches");
es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage();
std::shared_ptr<EnrichedMessage> enrichedMsg = std::shared_ptr<EnrichedMessage>(nullptr);
std::unique_ptr<EnrichedMessage> enrichedMsg = std::unique_ptr<EnrichedMessage>(nullptr);
auto mockEnricher = std::make_shared<MockEnricher>();
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(enrichedMsg));
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFile)).Times(1);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMatchesRegex)).Times(1);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMissesRegex)).Times(1);
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
@@ -145,11 +159,11 @@ class MockLogger : public Logger {
}]);
}
// CLOSE modified, remove from cache
// CLOSE modified, remove from cache, and matches fileChangesRegex
{
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
esMsg.event.close.modified = true;
esMsg.event.close.target = &targetFile;
esMsg.event.close.target = &targetFileMatchesRegex;
Message msg(mockESApi, &esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
@@ -164,10 +178,22 @@ class MockLogger : public Logger {
XCTAssertSemaTrue(sema, 5, "Log wasn't called within expected time window");
}
// CLOSE modified, remove from cache, but doesn't match fileChangesRegex
{
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
esMsg.event.close.modified = true;
esMsg.event.close.target = &targetFileMissesRegex;
Message msg(mockESApi, &esMsg);
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, &esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTFail("Metrics record callback should not be called here");
}]);
}
// LINK, Prefix match, bail early
{
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK;
esMsg.event.link.source = &targetFile;
esMsg.event.link.source = &targetFileMatchesRegex;
prefixTree->InsertPrefix(esMsg.event.link.source->path.data, Unit{});
Message msg(mockESApi, &esMsg);

View File

@@ -111,7 +111,6 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
- (void)enable {
[super enableTargetPathWatching];
[super unmuteEverything];
// Get the set of protected paths
std::set<std::string> protectedPaths = [SNTEndpointSecurityTamperResistance getProtectedPaths];

View File

@@ -65,7 +65,6 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
// Setup mocks to handle inverting target path muting
EXPECT_CALL(*mockESApi, InvertTargetPathMuting).WillOnce(testing::Return(true));
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
// Setup mocks to handle muting the rules db and events db

View File

@@ -26,6 +26,7 @@
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
#import "Source/santad/SNTDecisionCache.h"
// Forward declarations
@class SNTStoredEvent;
@@ -39,8 +40,8 @@ class Logger {
public:
static std::unique_ptr<Logger> Create(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
SNTEventLogType log_type, NSString *event_log_path, NSString *spool_log_path,
size_t spool_dir_size_threshold, size_t spool_file_size_threshold,
SNTEventLogType log_type, SNTDecisionCache *decision_cache, NSString *event_log_path,
NSString *spool_log_path, size_t spool_dir_size_threshold, size_t spool_file_size_threshold,
uint64_t spool_flush_timeout_ms);
Logger(std::shared_ptr<serializers::Serializer> serializer,
@@ -49,7 +50,7 @@ class Logger {
virtual ~Logger() = default;
virtual void Log(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg);
std::unique_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg);
void LogAllowlist(const santa::santad::event_providers::endpoint_security::Message &msg,
const std::string_view hash);

View File

@@ -20,10 +20,12 @@
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/File.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Spool.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h"
#include "Source/santad/SNTDecisionCache.h"
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
@@ -48,26 +50,33 @@ static const size_t kMaxExpectedWriteSizeBytes = 4096;
// Translate configured log type to appropriate Serializer/Writer pairs
std::unique_ptr<Logger> Logger::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
SNTEventLogType log_type, NSString *event_log_path,
NSString *spool_log_path, size_t spool_dir_size_threshold,
SNTEventLogType log_type, SNTDecisionCache *decision_cache,
NSString *event_log_path, NSString *spool_log_path,
size_t spool_dir_size_threshold,
size_t spool_file_size_threshold,
uint64_t spool_flush_timeout_ms) {
switch (log_type) {
case SNTEventLogTypeFilelog:
return std::make_unique<Logger>(
BasicString::Create(esapi),
BasicString::Create(esapi, std::move(decision_cache)),
File::Create(event_log_path, kFlushBufferTimeoutMS, kBufferBatchSizeBytes,
kMaxExpectedWriteSizeBytes));
case SNTEventLogTypeSyslog:
return std::make_unique<Logger>(BasicString::Create(esapi, false), Syslog::Create());
return std::make_unique<Logger>(BasicString::Create(esapi, std::move(decision_cache), false),
Syslog::Create());
case SNTEventLogTypeNull: return std::make_unique<Logger>(Empty::Create(), Null::Create());
case SNTEventLogTypeProtobuf:
LOGW(@"The EventLogType value protobuf is currently in beta. The protobuf schema is subject "
@"to change.");
return std::make_unique<Logger>(
Protobuf::Create(esapi),
Protobuf::Create(esapi, std::move(decision_cache)),
Spool::Create([spool_log_path UTF8String], spool_dir_size_threshold,
spool_file_size_threshold, spool_flush_timeout_ms));
case SNTEventLogTypeJSON:
return std::make_unique<Logger>(
Protobuf::Create(esapi, std::move(decision_cache), true),
File::Create(event_log_path, kFlushBufferTimeoutMS, kBufferBatchSizeBytes,
kMaxExpectedWriteSizeBytes));
default: LOGE(@"Invalid log type: %ld", log_type); return nullptr;
}
}
@@ -76,7 +85,7 @@ Logger::Logger(std::shared_ptr<serializers::Serializer> serializer,
std::shared_ptr<writers::Writer> writer)
: serializer_(std::move(serializer)), writer_(std::move(writer)) {}
void Logger::Log(std::shared_ptr<EnrichedMessage> msg) {
void Logger::Log(std::unique_ptr<EnrichedMessage> msg) {
writer_->Write(serializer_->SerializeMessage(std::move(msg)));
}

View File

@@ -103,28 +103,33 @@ class MockWriter : public Null {
// Ensure that the factory method creates expected serializers/writers pairs
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, @"/tmp/temppy",
XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, nil, @"/tmp/temppy",
@"/tmp/spool", 1, 1, 1));
LoggerPeer logger(
Logger::Create(mockESApi, SNTEventLogTypeFilelog, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
Logger::Create(mockESApi, SNTEventLogTypeFilelog, nil, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<File>(logger.Writer()));
logger = LoggerPeer(
Logger::Create(mockESApi, SNTEventLogTypeSyslog, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
Logger::Create(mockESApi, SNTEventLogTypeSyslog, nil, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Syslog>(logger.Writer()));
logger = LoggerPeer(
Logger::Create(mockESApi, SNTEventLogTypeNull, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
Logger::Create(mockESApi, SNTEventLogTypeNull, nil, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Empty>(logger.Serializer()));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Null>(logger.Writer()));
logger = LoggerPeer(
Logger::Create(mockESApi, SNTEventLogTypeProtobuf, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
logger = LoggerPeer(Logger::Create(mockESApi, SNTEventLogTypeProtobuf, nil, @"/tmp/temppy",
@"/tmp/spool", 1, 1, 1));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Protobuf>(logger.Serializer()));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Spool>(logger.Writer()));
logger = LoggerPeer(
Logger::Create(mockESApi, SNTEventLogTypeJSON, nil, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Protobuf>(logger.Serializer()));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<File>(logger.Writer()));
}
- (void)testLog {
@@ -136,16 +141,19 @@ class MockWriter : public Null {
es_message_t msg;
mockESApi->SetExpectationsRetainReleaseMessage();
auto enrichedMsg = std::make_shared<EnrichedMessage>(
EnrichedClose(Message(mockESApi, &msg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
EXPECT_CALL(*mockWriter, Write).Times(1);
{
auto enrichedMsg = std::make_unique<EnrichedMessage>(
EnrichedClose(Message(mockESApi, &msg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
Logger(mockSerializer, mockWriter).Log(enrichedMsg);
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
EXPECT_CALL(*mockWriter, Write).Times(1);
Logger(mockSerializer, mockWriter).Log(std::move(enrichedMsg));
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());

View File

@@ -24,6 +24,7 @@
#import "Source/common/SNTCachedDecision.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
#import "Source/santad/SNTDecisionCache.h"
namespace santa::santad::logs::endpoint_security::serializers {
@@ -31,11 +32,11 @@ class BasicString : public Serializer {
public:
static std::shared_ptr<BasicString> Create(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
bool prefix_time_name = true);
SNTDecisionCache *decision_cache, bool prefix_time_name = true);
BasicString(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
bool prefix_time_name);
SNTDecisionCache *decision_cache, bool prefix_time_name);
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;

View File

@@ -29,7 +29,6 @@
#include <string>
#import "Source/common/SNTCachedDecision.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTStoredEvent.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
@@ -92,11 +91,13 @@ std::string GetReasonString(SNTEventState event_state) {
case SNTEventStateAllowCertificate: return "CERT";
case SNTEventStateAllowScope: return "SCOPE";
case SNTEventStateAllowTeamID: return "TEAMID";
case SNTEventStateAllowSigningID: return "SIGNINGID";
case SNTEventStateAllowUnknown: return "UNKNOWN";
case SNTEventStateBlockBinary: return "BINARY";
case SNTEventStateBlockCertificate: return "CERT";
case SNTEventStateBlockScope: return "SCOPE";
case SNTEventStateBlockTeamID: return "TEAMID";
case SNTEventStateBlockSigningID: return "SIGNINGID";
case SNTEventStateBlockLongPath: return "LONG_PATH";
case SNTEventStateBlockUnknown: return "UNKNOWN";
default: return "NOTRUNNING";
@@ -177,12 +178,14 @@ static char *FormattedDateString(char *buf, size_t len) {
}
std::shared_ptr<BasicString> BasicString::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
SNTDecisionCache *decision_cache,
bool prefix_time_name) {
return std::make_shared<BasicString>(esapi, prefix_time_name);
return std::make_shared<BasicString>(esapi, decision_cache, prefix_time_name);
}
BasicString::BasicString(std::shared_ptr<EndpointSecurityAPI> esapi, bool prefix_time_name)
: esapi_(esapi), prefix_time_name_(prefix_time_name) {}
BasicString::BasicString(std::shared_ptr<EndpointSecurityAPI> esapi,
SNTDecisionCache *decision_cache, bool prefix_time_name)
: Serializer(std::move(decision_cache)), esapi_(esapi), prefix_time_name_(prefix_time_name) {}
std::string BasicString::CreateDefaultString(size_t reserved_size) {
std::string str;
@@ -245,9 +248,6 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg, SNTC
const es_message_t &esm = msg.es_msg();
std::string str = CreateDefaultString(1024); // EXECs tend to be bigger, reserve more space.
// Only need to grab the shared instance once
static SNTConfigurator *configurator = [SNTConfigurator configurator];
str.append("action=EXEC|decision=");
str.append(GetDecisionString(cd.decision));
str.append("|reason=");
@@ -291,7 +291,7 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg, SNTC
msg.instigator().real_group());
str.append("|mode=");
str.append(GetModeString([configurator clientMode]));
str.append(GetModeString(cd.decisionClientMode));
str.append("|path=");
str.append(FilePath(esm.event.exec.target->executable).Sanitized());

View File

@@ -56,10 +56,10 @@ using santa::santad::logs::endpoint_security::serializers::GetModeString;
using santa::santad::logs::endpoint_security::serializers::GetReasonString;
std::string BasicStringSerializeMessage(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
es_message_t *esMsg, SNTDecisionCache *decisionCache) {
mockESApi->SetExpectationsRetainReleaseMessage();
std::shared_ptr<Serializer> bs = BasicString::Create(mockESApi, false);
std::shared_ptr<Serializer> bs = BasicString::Create(mockESApi, decisionCache, false);
std::vector<uint8_t> ret = bs->SerializeMessage(Enricher().Enrich(Message(mockESApi, esMsg)));
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
@@ -69,7 +69,7 @@ std::string BasicStringSerializeMessage(std::shared_ptr<MockEndpointSecurityAPI>
std::string BasicStringSerializeMessage(es_message_t *esMsg) {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
return BasicStringSerializeMessage(mockESApi, esMsg);
return BasicStringSerializeMessage(mockESApi, esMsg, nil);
}
@interface BasicStringTest : XCTestCase
@@ -94,6 +94,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
self.testCachedDecision.sha256 = @"1234_hash";
self.testCachedDecision.quarantineURL = @"google.com";
self.testCachedDecision.certSHA256 = @"5678_hash";
self.testCachedDecision.decisionClientMode = SNTClientModeLockdown;
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
@@ -163,7 +164,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
.WillOnce(testing::Return(es_string_token_t{5, "-l\n-t"}))
.WillOnce(testing::Return(es_string_token_t{8, "-v\r--foo"}));
std::string got = BasicStringSerializeMessage(mockESApi, &esMsg);
std::string got = BasicStringSerializeMessage(mockESApi, &esMsg, self.mockDecisionCache);
std::string want =
"action=EXEC|decision=ALLOW|reason=BINARY|explain=extra!|sha256=1234_hash|"
"cert_sha256=5678_hash|cert_cn=|quarantine_url=google.com|pid=12|pidversion="
@@ -289,7 +290,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
mockESApi->SetExpectationsRetainReleaseMessage();
std::vector<uint8_t> ret =
BasicString::Create(nullptr, false)
BasicString::Create(nullptr, nil, false)
->SerializeFileAccess("v1.0", "pol_name", Message(mockESApi, &esMsg),
Enricher().Enrich(*esMsg.process), "file_target",
FileAccessPolicyDecision::kAllowedAuditOnly);
@@ -310,7 +311,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage();
std::vector<uint8_t> ret = BasicString::Create(mockESApi, false)
std::vector<uint8_t> ret = BasicString::Create(mockESApi, nil, false)
->SerializeAllowlist(Message(mockESApi, &esMsg), "test_hash");
XCTAssertTrue(testing::Mock::VerifyAndClearExpectations(mockESApi.get()),
@@ -333,7 +334,8 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
se.fileBundlePath = @"file_bundle_path";
se.filePath = @"file_path";
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeBundleHashingEvent(se);
std::vector<uint8_t> ret =
BasicString::Create(nullptr, nil, false)->SerializeBundleHashingEvent(se);
std::string got(ret.begin(), ret.end());
std::string want = "action=BUNDLE|sha256=file_hash"
@@ -360,7 +362,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(NO);
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskAppeared(props);
std::vector<uint8_t> ret = BasicString::Create(nullptr, nil, false)->SerializeDiskAppeared(props);
std::string got(ret.begin(), ret.end());
std::string want = "action=DISKAPPEAR|mount=path|volume=|bsdname=bsd|fs=apfs"
@@ -376,7 +378,8 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
@"DAMediaBSDName" : @"bsd",
};
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskDisappeared(props);
std::vector<uint8_t> ret =
BasicString::Create(nullptr, nil, false)->SerializeDiskDisappeared(props);
std::string got(ret.begin(), ret.end());
std::string want = "action=DISKDISAPPEAR|mount=path|volume=|bsdname=bsd|machineid=my_id\n";
@@ -418,6 +421,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
{SNTEventStateBlockCertificate, "CERT"},
{SNTEventStateBlockScope, "SCOPE"},
{SNTEventStateBlockTeamID, "TEAMID"},
{SNTEventStateBlockSigningID, "SIGNINGID"},
{SNTEventStateBlockLongPath, "LONG_PATH"},
{SNTEventStateAllowUnknown, "UNKNOWN"},
{SNTEventStateAllowBinary, "BINARY"},
@@ -427,6 +431,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
{SNTEventStateAllowTransitive, "TRANSITIVE"},
{SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"},
{SNTEventStateAllowTeamID, "TEAMID"},
{SNTEventStateAllowSigningID, "SIGNINGID"},
};
for (const auto &kv : stateToReason) {

View File

@@ -28,6 +28,7 @@ namespace santa::santad::logs::endpoint_security::serializers {
class Empty : public Serializer {
public:
static std::shared_ptr<Empty> Create();
Empty();
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;

View File

@@ -31,6 +31,8 @@ std::shared_ptr<Empty> Empty::Create() {
return std::make_shared<Empty>();
}
Empty::Empty() : Serializer(nil) {}
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedClose &msg) {
return {};
}

View File

@@ -25,16 +25,19 @@
#include "Source/common/santa_proto_include_wrapper.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
#import "Source/santad/SNTDecisionCache.h"
namespace santa::santad::logs::endpoint_security::serializers {
class Protobuf : public Serializer {
public:
static std::shared_ptr<Protobuf> Create(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
SNTDecisionCache *decision_cache, bool json = false);
Protobuf(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
SNTDecisionCache *decision_cache, bool json = false);
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
@@ -84,6 +87,9 @@ class Protobuf : public Serializer {
std::vector<uint8_t> FinalizeProto(::santa::pb::v1::SantaMessage *santa_msg);
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
// Toggle for transforming protobuf output to its JSON form.
// See https://protobuf.dev/programming-guides/proto3/#json
bool json_;
};
} // namespace santa::santad::logs::endpoint_security::serializers

View File

@@ -17,6 +17,8 @@
#include <EndpointSecurity/EndpointSecurity.h>
#include <Kernel/kern/cs_blobs.h>
#include <bsm/libbsm.h>
#include <google/protobuf/stubs/status.h>
#include <google/protobuf/util/json_util.h>
#include <mach/message.h>
#include <math.h>
#include <sys/proc_info.h>
@@ -28,7 +30,6 @@
#include <string_view>
#import "Source/common/SNTCachedDecision.h"
#import "Source/common/SNTConfigurator.h"
#include "Source/common/SNTLogging.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/String.h"
@@ -39,6 +40,8 @@
using google::protobuf::Arena;
using google::protobuf::Timestamp;
using google::protobuf::util::JsonPrintOptions;
using google::protobuf::util::MessageToJsonString;
using santa::common::NSStringToUTF8StringView;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
@@ -66,11 +69,14 @@ namespace pbv1 = ::santa::pb::v1;
namespace santa::santad::logs::endpoint_security::serializers {
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi) {
return std::make_shared<Protobuf>(esapi);
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
SNTDecisionCache *decision_cache, bool json) {
return std::make_shared<Protobuf>(esapi, std::move(decision_cache), json);
}
Protobuf::Protobuf(std::shared_ptr<EndpointSecurityAPI> esapi) : esapi_(esapi) {}
Protobuf::Protobuf(std::shared_ptr<EndpointSecurityAPI> esapi, SNTDecisionCache *decision_cache,
bool json)
: Serializer(std::move(decision_cache)), esapi_(esapi), json_(json) {}
static inline void EncodeTimestamp(Timestamp *timestamp, struct timespec ts) {
timestamp->set_seconds(ts.tv_sec);
@@ -287,11 +293,13 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
case SNTEventStateAllowCertificate: return ::pbv1::Execution::REASON_CERT;
case SNTEventStateAllowScope: return ::pbv1::Execution::REASON_SCOPE;
case SNTEventStateAllowTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
case SNTEventStateAllowSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
case SNTEventStateAllowUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
case SNTEventStateBlockBinary: return ::pbv1::Execution::REASON_BINARY;
case SNTEventStateBlockCertificate: return ::pbv1::Execution::REASON_CERT;
case SNTEventStateBlockScope: return ::pbv1::Execution::REASON_SCOPE;
case SNTEventStateBlockTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
case SNTEventStateBlockSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
case SNTEventStateBlockLongPath: return ::pbv1::Execution::REASON_LONG_PATH;
case SNTEventStateBlockUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
default: return ::pbv1::Execution::REASON_NOT_RUNNING;
@@ -384,6 +392,26 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
}
std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
if (this->json_) {
// TODO: Profile this. It's probably not the most efficient way to do this.
JsonPrintOptions options;
options.always_print_enums_as_ints = false;
options.always_print_primitive_fields = true;
options.preserve_proto_field_names = true;
std::string json;
google::protobuf::util::Status status = MessageToJsonString(*santa_msg, &json, options);
if (!status.ok()) {
LOGE(@"Failed to convert protobuf to JSON: %s", status.ToString().c_str());
}
std::vector<uint8_t> vec(json.begin(), json.end());
// Add a newline to the end of the JSON row.
vec.push_back('\n');
return vec;
}
std::vector<uint8_t> vec(santa_msg->ByteSizeLong());
santa_msg->SerializeWithCachedSizesToArray(vec.data());
return vec;
@@ -423,9 +451,6 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCach
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
// Only need to grab the shared instance once
static SNTConfigurator *configurator = [SNTConfigurator configurator];
GetDecisionEnum(cd.decision);
::pbv1::Execution *pb_exec = santa_msg->mutable_execution();
@@ -486,7 +511,7 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCach
pb_exec->set_decision(GetDecisionEnum(cd.decision));
pb_exec->set_reason(GetReasonEnum(cd.decision));
pb_exec->set_mode(GetModeEnum([configurator clientMode]));
pb_exec->set_mode(GetModeEnum(cd.decisionClientMode));
if (cd.certSHA256 || cd.certCommonName) {
EncodeCertificateInfo(pb_exec->mutable_certificate_info(), cd.certSHA256, cd.certCommonName);

View File

@@ -18,6 +18,7 @@
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <gmock/gmock.h>
#include <google/protobuf/util/json_util.h>
#include <gtest/gtest.h>
#include <sys/proc_info.h>
#include <sys/signal.h>
@@ -26,8 +27,6 @@
#include <uuid/uuid.h>
#include <cstring>
#include <google/protobuf/util/json_util.h>
#import "Source/common/SNTCachedDecision.h"
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
@@ -46,6 +45,7 @@
using google::protobuf::Timestamp;
using google::protobuf::util::JsonPrintOptions;
using google::protobuf::util::JsonStringToMessage;
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
using santa::santad::event_providers::endpoint_security::Enricher;
@@ -128,12 +128,6 @@ bool CompareTime(const Timestamp &timestamp, struct timespec ts) {
return timestamp.seconds() == ts.tv_sec && timestamp.nanos() == ts.tv_nsec;
}
void CheckSantaMessage(const ::pbv1::SantaMessage &santaMsg, const es_message_t &esMsg,
struct timespec enrichmentTime) {
XCTAssertTrue(CompareTime(santaMsg.processed_time(), enrichmentTime));
XCTAssertTrue(CompareTime(santaMsg.event_time(), esMsg.time));
}
const google::protobuf::Message &SantaMessageEvent(const ::pbv1::SantaMessage &santaMsg) {
switch (santaMsg.event_case()) {
case ::pbv1::SantaMessage::kExecution: return santaMsg.execution();
@@ -167,23 +161,34 @@ std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
return json;
}
void CheckProto(const ::pbv1::SantaMessage &santaMsg,
std::shared_ptr<EnrichedMessage> enrichedMsg) {
return std::visit(
[santaMsg](const EnrichedEventType &enrichedEvent) {
CheckSantaMessage(santaMsg, enrichedEvent.es_msg(), enrichedEvent.enrichment_time());
NSString *wantData = LoadTestJson(EventTypeToFilename(enrichedEvent.es_msg().event_type),
enrichedEvent.es_msg().version);
std::string got = ConvertMessageToJsonString(santaMsg);
NSDictionary *findDelta(NSDictionary *a, NSDictionary *b) {
NSMutableDictionary *delta = NSMutableDictionary.dictionary;
XCTAssertEqualObjects([NSString stringWithUTF8String:got.c_str()], wantData);
},
enrichedMsg->GetEnrichedMessage());
// Find objects in a that don't exist or are different in b.
[a enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
id otherObj = b[key];
if (![obj isEqual:otherObj]) {
delta[key] = obj;
}
}];
// Find objects in the other dictionary that don't exist in self
[b enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
id aObj = a[key];
if (!aObj) {
delta[key] = obj;
}
}];
return delta;
}
void SerializeAndCheck(es_event_type_t eventType,
void (^messageSetup)(std::shared_ptr<MockEndpointSecurityAPI>,
es_message_t *)) {
es_message_t *),
SNTDecisionCache *decisionCache, bool json = false) {
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
@@ -204,16 +209,62 @@ void SerializeAndCheck(es_event_type_t eventType,
messageSetup(mockESApi, &esMsg);
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
std::shared_ptr<EnrichedMessage> enrichedMsg = Enricher().Enrich(Message(mockESApi, &esMsg));
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi, decisionCache, json);
std::unique_ptr<EnrichedMessage> enrichedMsg = Enricher().Enrich(Message(mockESApi, &esMsg));
std::vector<uint8_t> vec = bs->SerializeMessage(enrichedMsg);
// Copy some values we need to check later before the object is moved out of this funciton
struct timespec enrichmentTime;
struct timespec msgTime;
NSString *wantData = std::visit(
[&msgTime, &enrichmentTime](const EnrichedEventType &enrichedEvent) {
msgTime = enrichedEvent.es_msg().time;
enrichmentTime = enrichedEvent.enrichment_time();
return LoadTestJson(EventTypeToFilename(enrichedEvent.es_msg().event_type),
enrichedEvent.es_msg().version);
},
enrichedMsg->GetEnrichedMessage());
std::vector<uint8_t> vec = bs->SerializeMessage(std::move(enrichedMsg));
std::string protoStr(vec.begin(), vec.end());
// if we're checking against JSON then we should already have a jsonified string and just need
// to
::pbv1::SantaMessage santaMsg;
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
std::string gotData;
CheckProto(santaMsg, enrichedMsg);
if (json) {
// Parse the jsonified string into the protobuf
// gotData = protoStr;
google::protobuf::util::JsonParseOptions options;
options.ignore_unknown_fields = true;
google::protobuf::util::Status status = JsonStringToMessage(protoStr, &santaMsg, options);
gotData = ConvertMessageToJsonString(santaMsg);
} else {
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
gotData = ConvertMessageToJsonString(santaMsg);
}
XCTAssertTrue(CompareTime(santaMsg.processed_time(), enrichmentTime));
XCTAssertTrue(CompareTime(santaMsg.event_time(), msgTime));
// Convert JSON strings to objects and compare each key-value set.
NSError *jsonError;
NSData *objectData = [wantData dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *wantJSONDict =
[NSJSONSerialization JSONObjectWithData:objectData
options:NSJSONReadingMutableContainers
error:&jsonError];
XCTAssertNil(jsonError, @"failed to parse want data as JSON");
NSDictionary *gotJSONDict = [NSJSONSerialization
JSONObjectWithData:[NSData dataWithBytes:gotData.data() length:gotData.length()]
options:NSJSONReadingMutableContainers
error:&jsonError];
XCTAssertNil(jsonError, @"failed to parse got data as JSON");
// XCTAssertEqualObjects([NSString stringWithUTF8String:gotData.c_str()], wantData);
NSDictionary *delta = findDelta(wantJSONDict, gotJSONDict);
XCTAssertEqualObjects(@{}, delta);
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
@@ -226,7 +277,7 @@ void SerializeAndCheckNonESEvents(
const Message &msg)) {
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage();
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi, nil);
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
cur_version++) {
@@ -280,6 +331,7 @@ void SerializeAndCheckNonESEvents(
self.testCachedDecision.sha256 = @"1234_file_hash";
self.testCachedDecision.quarantineURL = @"google.com";
self.testCachedDecision.certSHA256 = @"5678_cert_hash";
self.testCachedDecision.decisionClientMode = SNTClientModeLockdown;
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
@@ -293,25 +345,36 @@ void SerializeAndCheckNonESEvents(
[self.mockDecisionCache stopMocking];
}
- (void)serializeAndCheckEvent:(es_event_type_t)eventType
messageSetup:(void (^)(std::shared_ptr<MockEndpointSecurityAPI>,
es_message_t *))messageSetup
json:(BOOL)json {
SerializeAndCheck(eventType, messageSetup, self.mockDecisionCache, (bool)json);
}
- (void)testSerializeMessageClose {
__block es_file_t file = MakeESFile("close_file", MakeStat(300));
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_CLOSE,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.close.modified = true;
esMsg->event.close.target = &file;
});
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_CLOSE
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
esMsg->event.close.modified = true;
esMsg->event.close.target = &file;
}
json:NO];
}
- (void)testSerializeMessageExchange {
__block es_file_t file1 = MakeESFile("exchange_file_1", MakeStat(300));
__block es_file_t file2 = MakeESFile("exchange_file_1", MakeStat(400));
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.exchangedata.file1 = &file1;
esMsg->event.exchangedata.file2 = &file2;
});
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
esMsg->event.exchangedata.file1 = &file1;
esMsg->event.exchangedata.file2 = &file2;
}
json:NO];
}
- (void)testGetDecisionEnum {
@@ -335,7 +398,7 @@ void SerializeAndCheckNonESEvents(
};
for (const auto &kv : stateToDecision) {
XCTAssertEqual(GetDecisionEnum(kv.first), kv.second, @"Bad decision for state: %ld", kv.first);
XCTAssertEqual(GetDecisionEnum(kv.first), kv.second, @"Bad decision for state: %llu", kv.first);
}
}
@@ -348,6 +411,7 @@ void SerializeAndCheckNonESEvents(
{SNTEventStateBlockCertificate, ::pbv1::Execution::REASON_CERT},
{SNTEventStateBlockScope, ::pbv1::Execution::REASON_SCOPE},
{SNTEventStateBlockTeamID, ::pbv1::Execution::REASON_TEAM_ID},
{SNTEventStateBlockSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
{SNTEventStateBlockLongPath, ::pbv1::Execution::REASON_LONG_PATH},
{SNTEventStateAllowUnknown, ::pbv1::Execution::REASON_UNKNOWN},
{SNTEventStateAllowBinary, ::pbv1::Execution::REASON_BINARY},
@@ -357,10 +421,11 @@ void SerializeAndCheckNonESEvents(
{SNTEventStateAllowTransitive, ::pbv1::Execution::REASON_TRANSITIVE},
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::REASON_PENDING_TRANSITIVE},
{SNTEventStateAllowTeamID, ::pbv1::Execution::REASON_TEAM_ID},
{SNTEventStateAllowSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
};
for (const auto &kv : stateToReason) {
XCTAssertEqual(GetReasonEnum(kv.first), kv.second, @"Bad reason for state: %ld", kv.first);
XCTAssertEqual(GetReasonEnum(kv.first), kv.second, @"Bad reason for state: %llu", kv.first);
}
}
@@ -373,7 +438,7 @@ void SerializeAndCheckNonESEvents(
};
for (const auto &kv : clientModeToExecMode) {
XCTAssertEqual(GetModeEnum(kv.first), kv.second, @"Bad mode for state: %ld", kv.first);
XCTAssertEqual(GetModeEnum(kv.first), kv.second, @"Bad mode for client mode: %ld", kv.first);
}
}
@@ -413,45 +478,102 @@ void SerializeAndCheckNonESEvents(
procTarget.signing_id = MakeESStringToken("my_signing_id");
procTarget.team_id = MakeESStringToken("my_team_id");
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXEC, ^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
esMsg->event.exec.target = &procTarget;
esMsg->event.exec.cwd = &fileCwd;
esMsg->event.exec.script = &fileScript;
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXEC
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
esMsg->event.exec.target = &procTarget;
esMsg->event.exec.cwd = &fileCwd;
esMsg->event.exec.script = &fileScript;
// For version 5, simulate a "truncated" set of FDs
if (esMsg->version == 5) {
esMsg->event.exec.last_fd = 123;
} else {
esMsg->event.exec.last_fd = 3;
}
// For version 5, simulate a "truncated" set of FDs
if (esMsg->version == 5) {
esMsg->event.exec.last_fd = 123;
} else {
esMsg->event.exec.last_fd = 3;
}
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
EXPECT_CALL(*mockESApi, ExecArg)
.WillOnce(testing::Return(MakeESStringToken("exec_path")))
.WillOnce(testing::Return(MakeESStringToken("-l")))
.WillOnce(testing::Return(MakeESStringToken("--foo")));
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
EXPECT_CALL(*mockESApi, ExecArg)
.WillOnce(testing::Return(MakeESStringToken("exec_path")))
.WillOnce(testing::Return(MakeESStringToken("-l")))
.WillOnce(testing::Return(MakeESStringToken("--foo")));
EXPECT_CALL(*mockESApi, ExecEnvCount).WillOnce(testing::Return(2));
EXPECT_CALL(*mockESApi, ExecEnv)
.WillOnce(testing::Return(MakeESStringToken("ENV_PATH=/path/to/bin:/and/another")))
.WillOnce(testing::Return(MakeESStringToken("DEBUG=1")));
EXPECT_CALL(*mockESApi, ExecEnvCount).WillOnce(testing::Return(2));
EXPECT_CALL(*mockESApi, ExecEnv)
.WillOnce(
testing::Return(MakeESStringToken("ENV_PATH=/path/to/bin:/and/another")))
.WillOnce(testing::Return(MakeESStringToken("DEBUG=1")));
if (esMsg->version >= 4) {
EXPECT_CALL(*mockESApi, ExecFDCount).WillOnce(testing::Return(3));
EXPECT_CALL(*mockESApi, ExecFD)
.WillOnce(testing::Return(&fd1))
.WillOnce(testing::Return(&fd2))
.WillOnce(testing::Return(&fd3));
}
});
if (esMsg->version >= 4) {
EXPECT_CALL(*mockESApi, ExecFDCount).WillOnce(testing::Return(3));
EXPECT_CALL(*mockESApi, ExecFD)
.WillOnce(testing::Return(&fd1))
.WillOnce(testing::Return(&fd2))
.WillOnce(testing::Return(&fd3));
}
}
json:NO];
}
- (void)testSerializeMessageExecJSON {
es_file_t procFileTarget = MakeESFile("fooexec", MakeStat(300));
__block es_process_t procTarget =
MakeESProcess(&procFileTarget, MakeAuditToken(23, 45), MakeAuditToken(67, 89));
__block es_file_t fileCwd = MakeESFile("cwd", MakeStat(400));
__block es_file_t fileScript = MakeESFile("script.sh", MakeStat(500));
__block es_fd_t fd1 = {.fd = 1, .fdtype = PROX_FDTYPE_VNODE};
__block es_fd_t fd2 = {.fd = 2, .fdtype = PROX_FDTYPE_SOCKET};
__block es_fd_t fd3 = {.fd = 3, .fdtype = PROX_FDTYPE_PIPE, .pipe = {.pipe_id = 123}};
procTarget.codesigning_flags = CS_SIGNED | CS_HARD | CS_KILL;
memset(procTarget.cdhash, 'A', sizeof(procTarget.cdhash));
procTarget.signing_id = MakeESStringToken("my_signing_id");
procTarget.team_id = MakeESStringToken("my_team_id");
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXEC
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
esMsg->event.exec.target = &procTarget;
esMsg->event.exec.cwd = &fileCwd;
esMsg->event.exec.script = &fileScript;
// For version 5, simulate a "truncated" set of FDs
if (esMsg->version == 5) {
esMsg->event.exec.last_fd = 123;
} else {
esMsg->event.exec.last_fd = 3;
}
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
EXPECT_CALL(*mockESApi, ExecArg)
.WillOnce(testing::Return(MakeESStringToken("exec_path")))
.WillOnce(testing::Return(MakeESStringToken("-l")))
.WillOnce(testing::Return(MakeESStringToken("--foo")));
EXPECT_CALL(*mockESApi, ExecEnvCount).WillOnce(testing::Return(2));
EXPECT_CALL(*mockESApi, ExecEnv)
.WillOnce(
testing::Return(MakeESStringToken("ENV_PATH=/path/to/bin:/and/another")))
.WillOnce(testing::Return(MakeESStringToken("DEBUG=1")));
if (esMsg->version >= 4) {
EXPECT_CALL(*mockESApi, ExecFDCount).WillOnce(testing::Return(3));
EXPECT_CALL(*mockESApi, ExecFD)
.WillOnce(testing::Return(&fd1))
.WillOnce(testing::Return(&fd2))
.WillOnce(testing::Return(&fd3));
}
}
json:YES];
}
- (void)testSerializeMessageExit {
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXIT,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.exit.stat = W_EXITCODE(1, 0);
});
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXIT
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
esMsg->event.exit.stat = W_EXITCODE(1, 0);
}
json:NO];
}
- (void)testEncodeExitStatus {
@@ -484,10 +606,12 @@ void SerializeAndCheckNonESEvents(
MakeESProcess(&procFileChild, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
procChild.tty = &ttyFileChild;
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_FORK,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.fork.child = &procChild;
});
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_FORK
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
esMsg->event.fork.child = &procChild;
}
json:NO];
}
- (void)testSerializeMessageLink {
@@ -495,12 +619,14 @@ void SerializeAndCheckNonESEvents(
__block es_file_t fileTargetDir = MakeESFile("target_dir");
es_string_token_t targetTok = MakeESStringToken("target_file");
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_LINK,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.link.source = &fileSource;
esMsg->event.link.target_dir = &fileTargetDir;
esMsg->event.link.target_filename = targetTok;
});
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_LINK
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
esMsg->event.link.source = &fileSource;
esMsg->event.link.target_dir = &fileTargetDir;
esMsg->event.link.target_filename = targetTok;
}
json:NO];
}
- (void)testSerializeMessageRename {
@@ -508,30 +634,34 @@ void SerializeAndCheckNonESEvents(
__block es_file_t fileTargetDir = MakeESFile("target_dir");
es_string_token_t targetTok = MakeESStringToken("target_file");
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_RENAME,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.rename.source = &fileSource;
// Test new and existing destination types
if (esMsg->version == 4) {
esMsg->event.rename.destination.existing_file = &fileTargetDir;
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
} else {
esMsg->event.rename.destination.new_path.dir = &fileTargetDir;
esMsg->event.rename.destination.new_path.filename = targetTok;
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
}
});
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_RENAME
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
esMsg->event.rename.source = &fileSource;
// Test new and existing destination types
if (esMsg->version == 4) {
esMsg->event.rename.destination.existing_file = &fileTargetDir;
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
} else {
esMsg->event.rename.destination.new_path.dir = &fileTargetDir;
esMsg->event.rename.destination.new_path.filename = targetTok;
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
}
}
json:NO];
}
- (void)testSerializeMessageUnlink {
__block es_file_t fileTarget = MakeESFile("unlink_file", MakeStat(300));
__block es_file_t fileTargetParent = MakeESFile("unlink_file_parent", MakeStat(400));
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_UNLINK,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.unlink.target = &fileTarget;
esMsg->event.unlink.parent_dir = &fileTargetParent;
});
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_UNLINK
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
esMsg->event.unlink.target = &fileTarget;
esMsg->event.unlink.parent_dir = &fileTargetParent;
}
json:NO];
}
- (void)testGetAccessType {
@@ -608,7 +738,7 @@ void SerializeAndCheckNonESEvents(
se.fileBundlePath = @"file_bundle_path";
se.filePath = @"file_path";
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeBundleHashingEvent(se);
std::vector<uint8_t> vec = Protobuf::Create(nullptr, nil)->SerializeBundleHashingEvent(se);
std::string protoStr(vec.begin(), vec.end());
::pbv1::SantaMessage santaMsg;
@@ -643,7 +773,7 @@ void SerializeAndCheckNonESEvents(
@"DADeviceProtocol" : @"usb",
};
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskAppeared(props);
std::vector<uint8_t> vec = Protobuf::Create(nullptr, nil)->SerializeDiskAppeared(props);
std::string protoStr(vec.begin(), vec.end());
::pbv1::SantaMessage santaMsg;
@@ -680,7 +810,7 @@ void SerializeAndCheckNonESEvents(
@"DADeviceProtocol" : @"usb",
};
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskDisappeared(props);
std::vector<uint8_t> vec = Protobuf::Create(nullptr, nil)->SerializeDiskDisappeared(props);
std::string protoStr(vec.begin(), vec.end());
::pbv1::SantaMessage santaMsg;

View File

@@ -17,12 +17,14 @@
#import <Foundation/Foundation.h>
#include <functional>
#include <memory>
#include <vector>
#import "Source/common/SNTCachedDecision.h"
#import "Source/common/SNTCommonEnums.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#import "Source/santad/SNTDecisionCache.h"
@class SNTStoredEvent;
@@ -30,11 +32,11 @@ namespace santa::santad::logs::endpoint_security::serializers {
class Serializer {
public:
Serializer();
Serializer(SNTDecisionCache *decision_cache);
virtual ~Serializer() = default;
std::vector<uint8_t> SerializeMessage(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg) {
std::unique_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg) {
return std::visit([this](const auto &arg) { return this->SerializeMessageTemplate(arg); },
msg->GetEnrichedMessage());
}
@@ -96,6 +98,7 @@ class Serializer {
bool enabled_machine_id_ = false;
std::string machine_id_;
SNTDecisionCache *decision_cache_;
};
} // namespace santa::santad::logs::endpoint_security::serializers

View File

@@ -24,7 +24,7 @@ namespace es = santa::santad::event_providers::endpoint_security;
namespace santa::santad::logs::endpoint_security::serializers {
Serializer::Serializer() {
Serializer::Serializer(SNTDecisionCache *decision_cache) : decision_cache_(decision_cache) {
if ([[SNTConfigurator configurator] enableMachineIDDecoration]) {
enabled_machine_id_ = true;
machine_id_ = [[[SNTConfigurator configurator] machineID] UTF8String] ?: "";
@@ -46,17 +46,15 @@ std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExch
return SerializeMessage(msg);
}
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExec &msg) {
static SNTDecisionCache *decision_cache = [SNTDecisionCache sharedCache];
SNTCachedDecision *cd;
const es_message_t &es_msg = msg.es_msg();
if (es_msg.action_type == ES_ACTION_TYPE_NOTIFY &&
es_msg.action.notify.result.auth == ES_AUTH_RESULT_ALLOW) {
// For allowed execs, cached decision timestamps must be updated
cd = [decision_cache
cd = [decision_cache_
resetTimestampForCachedDecision:msg.es_msg().event.exec.target->executable->stat];
} else {
cd = [decision_cache cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
cd = [decision_cache_ cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
}
return SerializeMessage(msg, cd);

View File

@@ -69,6 +69,7 @@ santa_unit_test(
deps = [
":fsspool",
":fsspool_log_batch_writer",
"//Source/common:TestUtils",
"@OCMock",
],
)

View File

@@ -16,6 +16,7 @@
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <functional>
#include <limits>
@@ -156,6 +157,12 @@ std::string SpoolDirectory(absl::string_view base_dir) {
return absl::StrCat(base_dir, PathSeparator(), kSpoolDirName);
}
bool operator==(struct timespec a, struct timespec b) {
return a.tv_sec == b.tv_sec && a.tv_nsec == b.tv_nsec;
}
bool operator!=(struct timespec a, struct timespec b) { return !(a == b); }
} // namespace
FsSpoolWriter::FsSpoolWriter(absl::string_view base_dir, size_t max_spool_size)
@@ -197,6 +204,25 @@ std::string FsSpoolWriter::UniqueFilename() {
return result;
}
absl::StatusOr<size_t> FsSpoolWriter::EstimateSpoolDirSize() {
struct stat stats;
if (stat(spool_dir_.c_str(), &stats) < 0) {
return absl::ErrnoToStatus(errno, "failed to stat spool directory");
}
if (stats.st_mtimespec != spool_dir_last_mtime_) {
// Store the updated mtime
spool_dir_last_mtime_ = stats.st_mtimespec;
// Recompute the current estimated size
return EstimateDirSize(spool_dir_);
} else {
// If the spool's last modification time hasn't changed then
// re-use the current estimate.
return spool_size_estimate_;
}
}
absl::Status FsSpoolWriter::WriteMessage(absl::string_view msg) {
if (absl::Status status = BuildDirectoryStructureIfNeeded(); !status.ok()) {
return status; // << "can't create directory structure for writer";
@@ -209,7 +235,7 @@ absl::Status FsSpoolWriter::WriteMessage(absl::string_view msg) {
// Recompute the spool size if we think we are
// over the limit.
if (spool_size_estimate_ > max_spool_size_) {
absl::StatusOr<size_t> estimate = EstimateDirSize(spool_dir_);
absl::StatusOr<size_t> estimate = EstimateSpoolDirSize();
if (!estimate.ok()) {
return estimate.status(); // failed to recompute spool size
}

View File

@@ -25,6 +25,11 @@
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
// Forward declarations
namespace fsspool {
class FsSpoolWriterPeer;
}
namespace fsspool {
// Enqueues messages into the spool. Multiple concurrent writers can
@@ -42,10 +47,13 @@ class FsSpoolWriter {
// returns the UNAVAILABLE canonical code (which is retryable).
absl::Status WriteMessage(absl::string_view msg);
friend class fsspool::FsSpoolWriterPeer;
private:
const std::string base_dir_;
const std::string spool_dir_;
const std::string tmp_dir_;
struct timespec spool_dir_last_mtime_;
// Approximate maximum size of the spooling area, in bytes. If a message is
// being written to a spooling area which already contains more than
@@ -81,6 +89,10 @@ class FsSpoolWriter {
// Generates a unique filename by combining the random ID of
// this writer with a sequence number.
std::string UniqueFilename();
// Estimate the size of the spool directory. However, only recompute a new
// estimate if the spool directory has has a change to its modification time.
absl::StatusOr<size_t> EstimateSpoolDirSize();
};
// This class is thread-unsafe.

View File

@@ -33,7 +33,6 @@ FsSpoolLogBatchWriter::~FsSpoolLogBatchWriter() {
if (!s.ok()) {
os_log(OS_LOG_DEFAULT, "Flush() failed with %s",
s.ToString(absl::StatusToStringMode::kWithEverything).c_str());
// LOG(WARNING) << "Flush() failed with " << s;
}
}
@@ -56,7 +55,12 @@ absl::Status FsSpoolLogBatchWriter::FlushNoLock() {
return status;
}
}
cache_.mutable_records()->Clear();
// We assign a new LogBatch() object here instead of calling Clear() method to
// make sure the memory used by the cache_ is actually freed. It seems that
// internal implementation of protobuf has some very generous way of managing
// memory allocations and in certain scenarios it keeps objects for a very
// long time (forever?).
cache_ = santa::fsspool::binaryproto::LogBatch();
cache_.mutable_records()->Reserve(max_batch_size_);
return absl::OkStatus();
}

View File

@@ -62,7 +62,7 @@ class FsSpoolLogBatchWriter {
absl::Mutex cache_mutex_;
santa::fsspool::binaryproto::LogBatch cache_ ABSL_GUARDED_BY(cache_mutex_);
absl::Status FlushNoLock() ABSL_SHARED_LOCKS_REQUIRED(cache_mutex_);
absl::Status FlushNoLock() ABSL_EXCLUSIVE_LOCKS_REQUIRED(cache_mutex_);
};
} // namespace fsspool

View File

@@ -18,13 +18,31 @@
#include <memory>
#include "Source/common/TestUtils.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool_log_batch_writer.h"
#include "google/protobuf/any.pb.h"
#include "google/protobuf/timestamp.pb.h"
namespace fsspool {
class FsSpoolWriterPeer : public FsSpoolWriter {
public:
// Constructors
using FsSpoolWriter::FsSpoolWriter;
// Private Methods
using FsSpoolWriter::BuildDirectoryStructureIfNeeded;
using FsSpoolWriter::EstimateSpoolDirSize;
// Private member variables
using FsSpoolWriter::spool_size_estimate_;
};
} // namespace fsspool
using fsspool::FsSpoolLogBatchWriter;
using fsspool::FsSpoolWriter;
using fsspool::FsSpoolWriterPeer;
static constexpr size_t kSpoolSize = 1048576;
@@ -72,8 +90,65 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
XCTAssertTrue([self.fileMgr removeItemAtPath:self.testDir error:nil]);
}
- (void)testEstimateSpoolDirSize {
NSString *testData = @"What a day for some testing!";
NSString *largeTestData = RepeatedString(@"A", 10240);
NSString *path = [NSString stringWithFormat:@"%@/%@", self.spoolDir, @"temppy.log"];
NSString *emptyPath = [NSString stringWithFormat:@"%@/%@", self.spoolDir, @"empty.log"];
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
// Create the spool dir structure and ensure no files exist
XCTAssertStatusOk(writer->BuildDirectoryStructureIfNeeded());
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 0);
// Ensure that the initial spool dir estimate is 0
auto status = writer->EstimateSpoolDirSize();
XCTAssertStatusOk(status);
XCTAssertEqual(*status, 0);
// Force the current estimate to be 0 since we're not recomputing on first write.
writer->spool_size_estimate_ = *status;
XCTAssertTrue([testData writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil]);
// Ensure the test file was created
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 1);
// Ensure the spool size estimate has grown at least as much as the content length
status = writer->EstimateSpoolDirSize();
XCTAssertStatusOk(status);
// Update the current estimate
writer->spool_size_estimate_ = *status;
XCTAssertGreaterThanOrEqual(writer->spool_size_estimate_, testData.length);
// Modify file contents without modifying spool directory mtime
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:path];
[fileHandle seekToEndOfFile];
[fileHandle writeData:[largeTestData dataUsingEncoding:NSUTF8StringEncoding]];
[fileHandle closeFile];
// Ensure only one file still exists
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 1);
// Ensure that the returned estimate is the same as the old since mtime didn't change
status = writer->EstimateSpoolDirSize();
XCTAssertStatusOk(status);
// Check that the current estimate is the same as the old estimate
XCTAssertEqual(*status, writer->spool_size_estimate_);
// Create a second file in the spool dir to bump mtime
XCTAssertTrue([@"" writeToFile:emptyPath atomically:YES encoding:NSUTF8StringEncoding error:nil]);
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 2);
status = writer->EstimateSpoolDirSize();
XCTAssertStatusOk(status);
// Ensure the newly returned size is appropriate
XCTAssertGreaterThanOrEqual(*status, testData.length + largeTestData.length);
}
- (void)testSimpleWrite {
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.spoolDir]);
@@ -90,7 +165,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
}
- (void)testSpoolFull {
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
const std::string largeMessage(kSpoolSize + 1, '\x42');
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
@@ -121,7 +196,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
}
- (void)testWriteMessageNoFlush {
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
FsSpoolLogBatchWriter batch_writer(writer.get(), 10);
// Ensure that writing in batch mode doesn't flsuh on individual writes.
@@ -134,7 +209,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
- (void)testWriteMessageFlushAtCapacity {
static const int kCapacity = 5;
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
FsSpoolLogBatchWriter batch_writer(writer.get(), kCapacity);
// Ensure batch flushed once capacity exceeded
@@ -153,7 +228,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
static const int kCapacity = 5;
static const int kExpectedFlushes = 3;
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
FsSpoolLogBatchWriter batch_writer(writer.get(), kCapacity);
// Ensure batch flushed expected number of times
@@ -173,7 +248,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
static const int kCapacity = 10;
static const int kNumberOfWrites = 7;
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
{
// Extra scope to enforce early destroy of batch_writer.

View File

@@ -63,7 +63,8 @@ NSString *const ProcessorToString(Processor processor) {
case Processor::kTamperResistance: return kProcessorTamperResistance;
case Processor::kFileAccessAuthorizer: return kProcessorFileAccessAuthorizer;
default:
[NSException raise:@"Invalid processor" format:@"Unknown processor value: %d", processor];
[NSException raise:@"Invalid processor"
format:@"Unknown processor value: %d", static_cast<int>(processor)];
return nil;
}
}
@@ -103,7 +104,8 @@ NSString *const EventDispositionToString(EventDisposition d) {
case EventDisposition::kDropped: return kEventDispositionDropped;
case EventDisposition::kProcessed: return kEventDispositionProcessed;
default:
[NSException raise:@"Invalid disposition" format:@"Unknown disposition value: %d", d];
[NSException raise:@"Invalid disposition"
format:@"Unknown disposition value: %d", static_cast<int>(d)];
return nil;
}
}

View File

@@ -144,7 +144,10 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
// Check if there is an existing (non-transitive) rule for this file. We leave existing rules
// alone, so that a allowlist or blocklist rule can't be overwritten by a transitive one.
SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable];
SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256 certificateSHA256:nil teamID:nil];
SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256
signingID:nil
certificateSHA256:nil
teamID:nil];
if (!prevRule || prevRule.state == SNTRuleStateAllowTransitive) {
// Construct a new transitive allowlist rule for the executable.
SNTRule *rule = [[SNTRule alloc] initWithIdentifier:fi.SHA256

View File

@@ -98,10 +98,10 @@ double watchdogRAMPeak = 0;
#pragma mark Database ops
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
int64_t transitive, int64_t teamID))reply {
int64_t transitive, int64_t teamID, int64_t signingID))reply {
SNTRuleTable *rdb = [SNTDatabaseController ruleTable];
reply([rdb binaryRuleCount], [rdb certificateRuleCount], [rdb compilerRuleCount],
[rdb transitiveRuleCount], [rdb teamIDRuleCount]);
[rdb transitiveRuleCount], [rdb teamIDRuleCount], [rdb signingIDRuleCount]);
}
- (void)databaseRuleAddRules:(NSArray *)rules
@@ -144,8 +144,10 @@ double watchdogRAMPeak = 0;
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID
signingID:(NSString *)signingID
reply:(void (^)(SNTRule *))reply {
reply([[SNTDatabaseController ruleTable] ruleForBinarySHA256:binarySHA256
signingID:signingID
certificateSHA256:certificateSHA256
teamID:teamID]);
}
@@ -160,11 +162,13 @@ double watchdogRAMPeak = 0;
fileSHA256:(NSString *)fileSHA256
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID
signingID:(NSString *)signingID
reply:(void (^)(SNTEventState))reply {
reply([self.policyProcessor decisionForFilePath:filePath
fileSHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID]
teamID:teamID
signingID:signingID]
.decision);
}

View File

@@ -39,8 +39,6 @@ static NSString *const kEventsDatabaseName = @"events.db";
NSString *fullPath =
[[SNTDatabaseController databasePath] stringByAppendingPathComponent:kEventsDatabaseName];
FMDatabaseQueue *dbq = [[FMDatabaseQueue alloc] initWithPath:fullPath];
chown([fullPath UTF8String], 0, 0);
chmod([fullPath UTF8String], 0600);
#ifndef DEBUG
[dbq inDatabase:^(FMDatabase *db) {
@@ -49,6 +47,9 @@ static NSString *const kEventsDatabaseName = @"events.db";
#endif
eventDatabase = [[SNTEventTable alloc] initWithDatabaseQueue:dbq];
chown([fullPath UTF8String], 0, 0);
chmod([fullPath UTF8String], 0600);
});
return eventDatabase;
@@ -62,8 +63,6 @@ static NSString *const kEventsDatabaseName = @"events.db";
NSString *fullPath =
[[SNTDatabaseController databasePath] stringByAppendingPathComponent:kRulesDatabaseName];
FMDatabaseQueue *dbq = [[FMDatabaseQueue alloc] initWithPath:fullPath];
chown([fullPath UTF8String], 0, 0);
chmod([fullPath UTF8String], 0600);
#ifndef DEBUG
[dbq inDatabase:^(FMDatabase *db) {
@@ -72,6 +71,9 @@ static NSString *const kEventsDatabaseName = @"events.db";
#endif
ruleDatabase = [[SNTRuleTable alloc] initWithDatabaseQueue:dbq];
chown([fullPath UTF8String], 0, 0);
chmod([fullPath UTF8String], 0600);
});
return ruleDatabase;
}

View File

@@ -85,8 +85,8 @@ SNTCachedDecision *MakeCachedDecision(struct stat sb, SNTEventState decision) {
OCMExpect([self.mockRuleDatabase
resetTimestampForRule:[OCMArg checkWithBlock:^BOOL(SNTRule *rule) {
return rule.identifier == cd.sha256 && rule.state == SNTRuleStateAllowTransitive &&
rule.type == SNTRuleTypeBinary;
return [rule.identifier isEqualToString:cd.sha256] &&
rule.state == SNTRuleStateAllowTransitive && rule.type == SNTRuleTypeBinary;
}]]);
[dc resetTimestampForCachedDecision:sb];

View File

@@ -14,9 +14,9 @@
#import <Foundation/Foundation.h>
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#import "Source/common/SNTCommonEnums.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/TTYWriter.h"
const static NSString *kBlockBinary = @"BlockBinary";
const static NSString *kAllowBinary = @"AllowBinary";
@@ -24,6 +24,8 @@ const static NSString *kBlockCertificate = @"BlockCertificate";
const static NSString *kAllowCertificate = @"AllowCertificate";
const static NSString *kBlockTeamID = @"BlockTeamID";
const static NSString *kAllowTeamID = @"AllowTeamID";
const static NSString *kBlockSigningID = @"BlockSigningID";
const static NSString *kAllowSigningID = @"AllowSigningID";
const static NSString *kBlockScope = @"BlockScope";
const static NSString *kAllowScope = @"AllowScope";
const static NSString *kAllowUnknown = @"AllowUnknown";
@@ -54,7 +56,8 @@ const static NSString *kBlockLongPath = @"BlockLongPath";
- (instancetype)initWithRuleTable:(SNTRuleTable *)ruleTable
eventTable:(SNTEventTable *)eventTable
notifierQueue:(SNTNotificationQueue *)notifierQueue
syncdQueue:(SNTSyncdQueue *)syncdQueue;
syncdQueue:(SNTSyncdQueue *)syncdQueue
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter;
///
/// Handles the logic of deciding whether to allow the binary to run or not, sends the response to

View File

@@ -15,6 +15,7 @@
#import "Source/santad/SNTExecutionController.h"
#import <MOLCodesignChecker/MOLCodesignChecker.h>
#include <bsm/libbsm.h>
#include <copyfile.h>
#include <libproc.h>
@@ -22,7 +23,7 @@
#include <sys/param.h>
#include <utmpx.h>
#import <MOLCodesignChecker/MOLCodesignChecker.h>
#include <memory>
#include "Source/common/BranchPrediction.h"
#import "Source/common/SNTBlockMessage.h"
@@ -44,6 +45,7 @@
#import "Source/santad/SNTPolicyProcessor.h"
#import "Source/santad/SNTSyncdQueue.h"
using santa::santad::TTYWriter;
using santa::santad::event_providers::endpoint_security::Message;
static const size_t kMaxAllowedPathLength = MAXPATHLEN - 1; // -1 to account for null terminator
@@ -59,7 +61,9 @@ static const size_t kMaxAllowedPathLength = MAXPATHLEN - 1; // -1 to account fo
@property dispatch_queue_t eventQueue;
@end
@implementation SNTExecutionController
@implementation SNTExecutionController {
std::shared_ptr<TTYWriter> _ttyWriter;
}
static NSString *const kPrinterProxyPreMonterey =
(@"/System/Library/Frameworks/Carbon.framework/Versions/Current/"
@@ -74,13 +78,15 @@ static NSString *const kPrinterProxyPostMonterey =
- (instancetype)initWithRuleTable:(SNTRuleTable *)ruleTable
eventTable:(SNTEventTable *)eventTable
notifierQueue:(SNTNotificationQueue *)notifierQueue
syncdQueue:(SNTSyncdQueue *)syncdQueue {
syncdQueue:(SNTSyncdQueue *)syncdQueue
ttyWriter:(std::shared_ptr<TTYWriter>)ttyWriter {
self = [super init];
if (self) {
_ruleTable = ruleTable;
_eventTable = eventTable;
_notifierQueue = notifierQueue;
_syncdQueue = syncdQueue;
_ttyWriter = std::move(ttyWriter);
_policyProcessor = [[SNTPolicyProcessor alloc] initWithRuleTable:_ruleTable];
_eventQueue =
@@ -108,6 +114,8 @@ static NSString *const kPrinterProxyPostMonterey =
case SNTEventStateAllowCertificate: eventTypeStr = kAllowCertificate; break;
case SNTEventStateBlockTeamID: eventTypeStr = kBlockTeamID; break;
case SNTEventStateAllowTeamID: eventTypeStr = kAllowTeamID; break;
case SNTEventStateBlockSigningID: eventTypeStr = kBlockSigningID; break;
case SNTEventStateAllowSigningID: eventTypeStr = kAllowSigningID; break;
case SNTEventStateBlockScope: eventTypeStr = kBlockScope; break;
case SNTEventStateAllowScope: eventTypeStr = kAllowScope; break;
case SNTEventStateBlockUnknown: eventTypeStr = kBlockUnknown; break;
@@ -200,7 +208,8 @@ static NSString *const kPrinterProxyPostMonterey =
// TODO(markowsky): Maybe add a metric here for how many large executables we're seeing.
// if (binInfo.fileSize > SomeUpperLimit) ...
SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo];
SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo
targetProcess:targetProc];
cd.vnodeId = SantaVnode::VnodeForFile(targetProc->executable);
@@ -237,6 +246,7 @@ static NSString *const kPrinterProxyPostMonterey =
se.signingChain = cd.certChain;
se.teamID = cd.teamID;
se.signingID = cd.signingID;
se.pid = @(audit_token_to_pid(targetProc->audit_token));
se.ppid = @(audit_token_to_pid(targetProc->parent_audit_token));
se.parentName = @(esMsg.ParentProcessName().c_str());
@@ -294,22 +304,23 @@ static NSString *const kPrinterProxyPostMonterey =
NSAttributedString *s = [SNTBlockMessage attributedBlockMessageForEvent:se
customMessage:cd.customMsg];
if (targetProc->tty && targetProc->tty->path.length > 0 && !config.enableSilentTTYMode) {
if (targetProc->tty && targetProc->tty->path.length > 0 && !config.enableSilentTTYMode &&
self->_ttyWriter) {
NSMutableString *msg = [NSMutableString stringWithCapacity:1024];
[msg appendFormat:@"\n\033[1mSanta\033[0m\n\n%@\n\n", s.string];
[msg appendFormat:@"\033[1mPath: \033[0m %@\n"
@"\033[1mIdentifier:\033[0m %@\n"
@"\033[1mParent: \033[0m %@ (%@)\n\n",
se.filePath, se.fileSHA256, se.parentName, se.ppid];
NSURL *detailURL = [SNTBlockMessage eventDetailURLForEvent:se];
NSURL *detailURL = [SNTBlockMessage eventDetailURLForEvent:se customURL:cd.customURL];
if (detailURL) {
[msg appendFormat:@"More info:\n%@\n\n", detailURL.absoluteString];
}
[self printMessage:msg toTTY:targetProc->tty->path.data];
self->_ttyWriter->Write(targetProc->tty->path.data, msg);
}
[self.notifierQueue addEvent:se customMessage:cd.customMsg];
[self.notifierQueue addEvent:se withCustomMessage:cd.customMsg andCustomURL:cd.customURL];
}
}
}
@@ -359,13 +370,6 @@ static NSString *const kPrinterProxyPostMonterey =
return proxyInfo;
}
- (void)printMessage:(NSString *)msg toTTY:(const char *)path {
int fd = open(path, O_WRONLY | O_NOCTTY);
std::string_view str = santa::common::NSStringToUTF8StringView(msg);
write(fd, str.data(), str.length());
close(fd);
}
- (void)loggedInUsers:(NSArray **)users sessions:(NSArray **)sessions {
NSMutableSet *loggedInUsers = [NSMutableSet set];
NSMutableArray *loggedInHosts = [NSMutableArray array];

View File

@@ -39,6 +39,9 @@ using santa::santad::event_providers::endpoint_security::Message;
using PostActionBlock = bool (^)(SNTAction);
using VerifyPostActionBlock = PostActionBlock (^)(SNTAction);
static const char *kExampleSigningID = "example.signing.id";
static const char *kExampleTeamID = "myteamid";
VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction) {
return ^bool(SNTAction gotAction) {
XCTAssertEqual(gotAction, wantAction);
@@ -92,7 +95,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
self.sut = [[SNTExecutionController alloc] initWithRuleTable:self.mockRuleDatabase
eventTable:self.mockEventDatabase
notifierQueue:nil
syncdQueue:nil];
syncdQueue:nil
ttyWriter:nullptr];
}
- (void)tearDown {
@@ -186,7 +190,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)validateExecEvent:(SNTAction)wantAction {
- (void)validateExecEvent:(SNTAction)wantAction
messageSetup:(void (^)(es_message_t *))messageSetupBlock {
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_file_t fileExec = MakeESFile("bar", {
@@ -194,9 +199,14 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
.st_ino = 34,
});
es_process_t procExec = MakeESProcess(&fileExec);
procExec.is_platform_binary = false;
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc);
esMsg.event.exec.target = &procExec;
if (messageSetupBlock) {
messageSetupBlock(&esMsg);
}
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage();
@@ -208,6 +218,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)validateExecEvent:(SNTAction)wantAction {
[self validateExecEvent:wantAction messageSetup:nil];
}
- (void)testBinaryAllowRule {
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
@@ -215,7 +229,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllow;
rule.type = SNTRuleTypeBinary;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
signingID:nil
certificateSHA256:nil
teamID:nil])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondAllow];
@@ -229,13 +246,102 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateBlock;
rule.type = SNTRuleTypeBinary;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
signingID:nil
certificateSHA256:nil
teamID:nil])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondDeny];
[self checkMetricCounters:kBlockBinary expected:@1];
}
- (void)testSigningIDAllowRule {
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllow;
rule.type = SNTRuleTypeSigningID;
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
signingID:signingID
certificateSHA256:nil
teamID:@(kExampleTeamID)])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondAllow
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->signing_id = MakeESStringToken(kExampleSigningID);
msg->event.exec.target->team_id = MakeESStringToken(kExampleTeamID);
}];
[self checkMetricCounters:kAllowSigningID expected:@1];
}
- (void)testSigningIDBlockRule {
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateBlock;
rule.type = SNTRuleTypeSigningID;
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
signingID:signingID
certificateSHA256:nil
teamID:@(kExampleTeamID)])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondDeny
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->signing_id = MakeESStringToken(kExampleSigningID);
msg->event.exec.target->team_id = MakeESStringToken(kExampleTeamID);
}];
[self checkMetricCounters:kBlockSigningID expected:@1];
}
- (void)testTeamIDAllowRule {
OCMStub([self.mockCodesignChecker signingInformation]).andReturn((@{
(__bridge NSString *)kSecCodeInfoTeamIdentifier : @(kExampleTeamID),
}));
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllow;
rule.type = SNTRuleTypeTeamID;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:nil
teamID:@(kExampleTeamID)])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondAllow
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kExampleTeamID);
}];
[self checkMetricCounters:kAllowTeamID expected:@1];
}
- (void)testTeamIDBlockRule {
OCMStub([self.mockCodesignChecker signingInformation]).andReturn((@{
(__bridge NSString *)kSecCodeInfoTeamIdentifier : @(kExampleTeamID),
}));
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateBlock;
rule.type = SNTRuleTypeTeamID;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:nil
teamID:@(kExampleTeamID)])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondDeny
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kExampleTeamID);
}];
[self checkMetricCounters:kBlockTeamID expected:@1];
}
- (void)testCertificateAllowRule {
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
@@ -246,7 +352,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllow;
rule.type = SNTRuleTypeCertificate;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil])
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:@"a"
teamID:nil])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondAllow];
@@ -263,7 +372,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateBlock;
rule.type = SNTRuleTypeCertificate;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil])
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:@"a"
teamID:nil])
.andReturn(rule);
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
@@ -282,7 +394,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllowCompiler;
rule.type = SNTRuleTypeBinary;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
signingID:nil
certificateSHA256:nil
teamID:nil])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondAllowCompiler];
@@ -297,7 +412,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllowCompiler;
rule.type = SNTRuleTypeBinary;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
signingID:nil
certificateSHA256:nil
teamID:nil])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondAllow];
@@ -312,7 +430,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllowTransitive;
rule.type = SNTRuleTypeBinary;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
signingID:nil
certificateSHA256:nil
teamID:nil])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondAllow];
@@ -328,7 +449,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllowTransitive;
rule.type = SNTRuleTypeBinary;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
signingID:nil
certificateSHA256:nil
teamID:nil])
.andReturn(rule);
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
@@ -439,7 +563,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllow;
rule.type = SNTRuleTypeBinary;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
signingID:nil
certificateSHA256:nil
teamID:nil])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondAllow];

View File

@@ -21,6 +21,8 @@
@property(nonatomic) MOLXPCConnection *notifierConnection;
- (void)addEvent:(SNTStoredEvent *)event customMessage:(NSString *)message;
- (void)addEvent:(SNTStoredEvent *)event
withCustomMessage:(NSString *)message
andCustomURL:(NSString *)url;
@end

View File

@@ -36,18 +36,21 @@ static const int kMaximumNotifications = 10;
return self;
}
- (void)addEvent:(SNTStoredEvent *)event customMessage:(NSString *)message {
- (void)addEvent:(SNTStoredEvent *)event
withCustomMessage:(NSString *)message
andCustomURL:(NSString *)url {
if (!event) return;
if (self.pendingNotifications.count > kMaximumNotifications) {
LOGI(@"Pending GUI notification count is over %d, dropping.", kMaximumNotifications);
return;
}
NSDictionary *d;
NSMutableDictionary *d = [@{@"event" : event} mutableCopy];
if (message) {
d = @{@"event" : event, @"message" : message};
} else {
d = @{@"event" : event};
d[@"message"] = message;
}
if (url) {
d[@"url"] = url;
}
@synchronized(self.pendingNotifications) {
[self.pendingNotifications addObject:d];
@@ -62,7 +65,9 @@ static const int kMaximumNotifications = 10;
@synchronized(self.pendingNotifications) {
NSMutableArray *postedNotifications = [NSMutableArray array];
for (NSDictionary *d in self.pendingNotifications) {
[rop postBlockNotification:d[@"event"] withCustomMessage:d[@"message"]];
[rop postBlockNotification:d[@"event"]
withCustomMessage:d[@"message"]
andCustomURL:d[@"url"]];
[postedNotifications addObject:d];
}
[self.pendingNotifications removeObjectsInArray:postedNotifications];

View File

@@ -12,6 +12,7 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#import <MOLCertificate/MOLCertificate.h>
@@ -44,10 +45,13 @@
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
fileSHA256:(nullable NSString *)fileSHA256
certificateSHA256:(nullable NSString *)certificateSHA256
teamID:(nullable NSString *)teamID;
teamID:(nullable NSString *)teamID
signingID:(nullable NSString *)signingID;
/// Convenience initializer with nil hashes for both the file and certificate.
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo;
/// Convenience initializer. Will obtain the teamID and construct the signingID
/// identifier if able.
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
targetProcess:(nonnull const es_process_t *)targetProc;
///
/// A wrapper for decisionForFileInfo:fileSHA256:certificateSHA256:. This method is slower as it
@@ -58,6 +62,7 @@
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
fileSHA256:(nullable NSString *)fileSHA256
certificateSHA256:(nullable NSString *)certificateSHA256
teamID:(nullable NSString *)teamID;
teamID:(nullable NSString *)teamID
signingID:(nullable NSString *)signingID;
@end

View File

@@ -15,6 +15,7 @@
#import "Source/santad/SNTPolicyProcessor.h"
#import <MOLCodesignChecker/MOLCodesignChecker.h>
#import <Security/SecCode.h>
#include "Source/common/SNTLogging.h"
@@ -41,10 +42,12 @@
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
fileSHA256:(nullable NSString *)fileSHA256
certificateSHA256:(nullable NSString *)certificateSHA256
teamID:(nullable NSString *)teamID {
teamID:(nullable NSString *)teamID
signingID:(nullable NSString *)signingID {
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
cd.sha256 = fileSHA256 ?: fileInfo.SHA256;
cd.teamID = teamID;
cd.signingID = signingID;
// If the binary is a critical system binary, don't check its signature.
// The binary was validated at startup when the rule table was initialized.
@@ -62,17 +65,34 @@
csInfo = nil;
cd.decisionExtra =
[NSString stringWithFormat:@"Signature ignored due to error: %ld", (long)csInfoError.code];
cd.teamID = nil;
cd.signingID = nil;
} else {
cd.certSHA256 = csInfo.leafCertificate.SHA256;
cd.certCommonName = csInfo.leafCertificate.commonName;
cd.certChain = csInfo.certificates;
cd.teamID = teamID ?: [csInfo.signingInformation valueForKey:@"teamid"];
cd.teamID = teamID
?: [csInfo.signingInformation
objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
teamID = cd.teamID;
// Ensure that if no teamID exists that the signing info confirms it is a
// platform binary. If not, remove the signingID.
if (!teamID && signingID) {
id platformID = [csInfo.signingInformation
objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
if (![platformID isKindOfClass:[NSNumber class]] || [platformID intValue] == 0) {
signingID = nil;
}
}
cd.signingID = signingID;
}
}
cd.quarantineURL = fileInfo.quarantineDataURL;
SNTRule *rule = [self.ruleTable ruleForBinarySHA256:cd.sha256
signingID:signingID
certificateSHA256:cd.certSHA256
teamID:teamID];
if (rule) {
@@ -83,6 +103,7 @@
case SNTRuleStateSilentBlock: cd.silentBlock = YES;
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockBinary;
return cd;
case SNTRuleStateAllowCompiler:
@@ -108,6 +129,20 @@
default: break;
}
break;
case SNTRuleTypeSigningID:
switch (rule.state) {
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowSigningID; return cd;
case SNTRuleStateSilentBlock:
cd.silentBlock = YES;
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockSigningID;
return cd;
default: break;
}
break;
case SNTRuleTypeCertificate:
switch (rule.state) {
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCertificate; return cd;
@@ -116,6 +151,7 @@
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockCertificate;
return cd;
default: break;
@@ -129,6 +165,7 @@
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockTeamID;
return cd;
default: break;
@@ -161,22 +198,47 @@
return cd;
}
switch ([[SNTConfigurator configurator] clientMode]) {
SNTClientMode mode = [[SNTConfigurator configurator] clientMode];
cd.decisionClientMode = mode;
switch (mode) {
case SNTClientModeMonitor: cd.decision = SNTEventStateAllowUnknown; return cd;
case SNTClientModeLockdown: cd.decision = SNTEventStateBlockUnknown; return cd;
default: cd.decision = SNTEventStateBlockUnknown; return cd;
}
}
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo {
return [self decisionForFileInfo:fileInfo fileSHA256:nil certificateSHA256:nil teamID:nil];
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
targetProcess:(nonnull const es_process_t *)targetProc {
NSString *signingID;
NSString *teamID;
if (targetProc->signing_id.length > 0) {
if (targetProc->team_id.length > 0) {
teamID = [NSString stringWithUTF8String:targetProc->team_id.data];
signingID =
[NSString stringWithFormat:@"%@:%@", teamID,
[NSString stringWithUTF8String:targetProc->signing_id.data]];
} else if (targetProc->is_platform_binary) {
signingID =
[NSString stringWithFormat:@"platform:%@",
[NSString stringWithUTF8String:targetProc->signing_id.data]];
}
}
return [self decisionForFileInfo:fileInfo
fileSHA256:nil
certificateSHA256:nil
teamID:teamID
signingID:signingID];
}
// Used by `$ santactl fileinfo`.
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
fileSHA256:(nullable NSString *)fileSHA256
certificateSHA256:(nullable NSString *)certificateSHA256
teamID:(nullable NSString *)teamID {
teamID:(nullable NSString *)teamID
signingID:(nullable NSString *)signingID {
SNTFileInfo *fileInfo;
NSError *error;
fileInfo = [[SNTFileInfo alloc] initWithPath:filePath error:&error];
@@ -184,7 +246,8 @@
return [self decisionForFileInfo:fileInfo
fileSHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID];
teamID:teamID
signingID:signingID];
}
///

View File

@@ -29,6 +29,7 @@
#import "Source/santad/SNTExecutionController.h"
#import "Source/santad/SNTNotificationQueue.h"
#import "Source/santad/SNTSyncdQueue.h"
#include "Source/santad/TTYWriter.h"
void SantadMain(
std::shared_ptr<
@@ -45,7 +46,7 @@ void SantadMain(
SNTCompilerController* compiler_controller,
SNTNotificationQueue* notifier_queue, SNTSyncdQueue* syncd_queue,
SNTExecutionController* exec_controller,
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>
prefix_tree);
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree,
std::shared_ptr<santa::santad::TTYWriter> tty_writer);
#endif

View File

@@ -20,6 +20,7 @@
#include "Source/common/PrefixTree.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTFileAccessEvent.h"
#import "Source/common/SNTKVOManager.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTXPCNotifierInterface.h"
@@ -36,10 +37,12 @@
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/santad/SNTDaemonControlController.h"
#include "Source/santad/SNTDecisionCache.h"
#include "Source/santad/TTYWriter.h"
using santa::common::PrefixTree;
using santa::common::Unit;
using santa::santad::Metrics;
using santa::santad::TTYWriter;
using santa::santad::data_layer::WatchItems;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
@@ -78,7 +81,8 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
MOLXPCConnection *control_connection, SNTCompilerController *compiler_controller,
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller,
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree) {
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree,
std::shared_ptr<TTYWriter> tty_writer) {
SNTConfigurator *configurator = [SNTConfigurator configurator];
SNTDaemonControlController *dc =
@@ -130,18 +134,28 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
SNTEndpointSecurityTamperResistance *tamper_client =
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:esapi metrics:metrics logger:logger];
SNTEndpointSecurityFileAccessAuthorizer *access_authorizer_client =
[[SNTEndpointSecurityFileAccessAuthorizer alloc] initWithESAPI:esapi
metrics:metrics
logger:logger
watchItems:watch_items
enricher:enricher
decisionCache:[SNTDecisionCache sharedCache]];
watch_items->RegisterClient(access_authorizer_client);
if (@available(macOS 13.0, *)) {
SNTEndpointSecurityFileAccessAuthorizer *access_authorizer_client =
[[SNTEndpointSecurityFileAccessAuthorizer alloc] initWithESAPI:esapi
metrics:metrics
logger:logger
watchItems:watch_items
enricher:enricher
decisionCache:[SNTDecisionCache sharedCache]
ttyWriter:tty_writer];
watch_items->RegisterClient(access_authorizer_client);
access_authorizer_client.fileAccessBlockCallback = ^(SNTFileAccessEvent *event) {
[[notifier_queue.notifierConnection remoteObjectProxy]
postFileAccessBlockNotification:event
withCustomMessage:@"Access to the resource has been denied!"];
};
}
EstablishSyncServiceConnection(syncd_queue);
NSArray<SNTKVOManager *> *kvoObservers = @[
NSMutableArray<SNTKVOManager *> *kvoObservers = [[NSMutableArray alloc] init];
[kvoObservers addObjectsFromArray:@[
[[SNTKVOManager alloc]
initWithObject:configurator
selector:@selector(clientMode)
@@ -294,37 +308,17 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
[newValue componentsJoinedByString:@","]);
device_client.remountArgs = newValue;
}],
[[SNTKVOManager alloc]
initWithObject:configurator
selector:@selector(fileAccessPolicyPlist)
type:[NSString class]
callback:^(NSString *oldValue, NSString *newValue) {
if ([configurator fileAccessPolicy]) {
// Ignore any changes to this key if fileAccessPolicy is set
return;
}
if (oldValue != newValue || (newValue && ![oldValue isEqualToString:newValue])) {
LOGI(@"Filesystem monitoring policy config path changed: %@ -> %@", oldValue,
newValue);
watch_items->SetConfigPath(newValue);
}
}],
[[SNTKVOManager alloc] initWithObject:configurator
selector:@selector(fileAccessPolicy)
type:[NSDictionary class]
callback:^(NSDictionary *oldValue, NSDictionary *newValue) {
if (oldValue != newValue ||
(newValue && ![oldValue isEqualToDictionary:newValue])) {
LOGI(@"Filesystem monitoring policy embedded config changed");
watch_items->SetConfig(newValue);
}
}],
[[SNTKVOManager alloc] initWithObject:configurator
selector:@selector(staticRules)
type:[NSArray class]
callback:^(NSArray *oldValue, NSArray *newValue) {
if ([oldValue isEqual:newValue]) return;
NSSet *oldValueSet = [NSSet setWithArray:oldValue ?: @[]];
NSSet *newValueSet = [NSSet setWithArray:newValue ?: @[]];
if ([oldValueSet isEqualToSet:newValueSet]) {
return;
}
LOGI(@"StaticRules set has changed, flushing cache.");
auth_result_cache->FlushCache(
FlushCacheMode::kAllCaches,
@@ -360,7 +354,40 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
// Forcefully exit. The daemon will be restarted immediately.
exit(EXIT_SUCCESS);
}],
];
]];
if (@available(macOS 13.0, *)) {
// Only watch file access auth keys on mac 13 and newer
[kvoObservers addObjectsFromArray:@[
[[SNTKVOManager alloc]
initWithObject:configurator
selector:@selector(fileAccessPolicyPlist)
type:[NSString class]
callback:^(NSString *oldValue, NSString *newValue) {
if ([configurator fileAccessPolicy]) {
// Ignore any changes to this key if fileAccessPolicy is set
return;
}
if ((oldValue && !newValue) || (newValue && ![oldValue isEqualToString:newValue])) {
LOGI(@"Filesystem monitoring policy config path changed: %@ -> %@", oldValue,
newValue);
watch_items->SetConfigPath(newValue);
}
}],
[[SNTKVOManager alloc] initWithObject:configurator
selector:@selector(fileAccessPolicy)
type:[NSDictionary class]
callback:^(NSDictionary *oldValue, NSDictionary *newValue) {
if ((oldValue && !newValue) ||
(newValue && ![oldValue isEqualToDictionary:newValue])) {
LOGI(
@"Filesystem monitoring policy embedded config changed");
watch_items->SetConfig(newValue);
}
}],
]];
}
// Make the compiler happy. The variable is only used to ensure proper lifetime
// of the SNTKVOManager objects it contains.

Some files were not shown because too many files have changed in this diff Show More