mirror of
https://github.com/google/santa.git
synced 2026-01-19 11:08:38 -05:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e124f4c51 | ||
|
|
cd719ccef4 | ||
|
|
dde42ee686 | ||
|
|
d144e27798 | ||
|
|
afc2c216b8 | ||
|
|
03d7556f22 | ||
|
|
020827b091 | ||
|
|
baa31a5db0 | ||
|
|
9ba7075596 | ||
|
|
5d08538639 | ||
|
|
e73bafb596 | ||
|
|
1e92d109a7 | ||
|
|
6a6aa6dce8 | ||
|
|
0715033d6a | ||
|
|
123d7a2d6a | ||
|
|
7b4d997589 | ||
|
|
5307bd9b7f | ||
|
|
0622e6de71 | ||
|
|
e7c32ae87d | ||
|
|
deaf3a638c | ||
|
|
8a7f1142a8 | ||
|
|
c180205059 | ||
|
|
337df0aa31 | ||
|
|
e2b099aa50 | ||
|
|
fc4e29f34c | ||
|
|
bf3b6bc6e2 | ||
|
|
b810fc81e1 | ||
|
|
3b3aa999c5 | ||
|
|
59428f3be3 | ||
|
|
ae6451a9b2 | ||
|
|
feac080fa7 | ||
|
|
d0f2a0ac4d | ||
|
|
7fc06ea9d8 | ||
|
|
1dfeeac936 | ||
|
|
ac9b5d9399 | ||
|
|
7f3f1c5448 | ||
|
|
46efd6893f | ||
|
|
50232578d6 | ||
|
|
d83be03a20 | ||
|
|
119b29b534 |
4
.bazelrc
4
.bazelrc
@@ -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
|
||||
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/sanitizers.yml
vendored
2
.github/workflows/sanitizers.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
DYLIB_PATH="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.${{ matrix.sanitizer }}_osx_dynamic.dylib"
|
||||
|
||||
bazel test --config=${{ matrix.sanitizer }} \
|
||||
--test_strategy=exclusive --test_output=errors \
|
||||
--test_strategy=exclusive --test_output=all \
|
||||
--test_env=DYLD_INSERT_LIBRARIES=${DYLIB_PATH} \
|
||||
--runs_per_test 5 -t- :unit_tests \
|
||||
--define=SANTA_BUILD_TYPE=adhoc
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
# Santa
|
||||
|
||||
[](https://github.com/google/santa/blob/main/LICENSE)
|
||||
[](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
|
||||
14
SECURITY.md
14
SECURITY.md
@@ -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`
|
||||
|
||||
@@ -69,6 +69,11 @@ objc_library(
|
||||
hdrs = ["Platform.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "String",
|
||||
hdrs = ["String.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SantaVnodeHash",
|
||||
srcs = ["SantaVnodeHash.mm"],
|
||||
@@ -128,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"],
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
@property SantaVnode vnodeId;
|
||||
@property SNTEventState decision;
|
||||
@property SNTClientMode decisionClientMode;
|
||||
@property NSString *decisionExtra;
|
||||
@property NSString *sha256;
|
||||
|
||||
@@ -36,6 +37,7 @@
|
||||
@property NSString *certCommonName;
|
||||
@property NSArray<MOLCertificate *> *certChain;
|
||||
@property NSString *teamID;
|
||||
@property NSString *signingID;
|
||||
|
||||
@property NSString *quarantineURL;
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -129,6 +140,12 @@ typedef NS_ENUM(NSInteger, SNTSyncStatusType) {
|
||||
SNTSyncStatusTypeUnknown,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTSyncContentEncoding) {
|
||||
SNTSyncContentEncodingNone,
|
||||
SNTSyncContentEncodingDeflate,
|
||||
SNTSyncContentEncodingGzip,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
|
||||
SNTMetricFormatTypeUnknown,
|
||||
SNTMetricFormatTypeRawJSON,
|
||||
|
||||
@@ -291,6 +291,14 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSilentMode;
|
||||
|
||||
///
|
||||
/// When silent TTY mode is enabled, Santa will not emit TTY notifications for
|
||||
/// blocked processes.
|
||||
///
|
||||
/// Defaults to NO.
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSilentTTYMode;
|
||||
|
||||
///
|
||||
/// The text to display when opening Santa.app.
|
||||
/// If unset, the default text will be displayed.
|
||||
@@ -402,6 +410,8 @@
|
||||
///
|
||||
@property(nonatomic) BOOL syncCleanRequired;
|
||||
|
||||
#pragma mark - USB Settings
|
||||
|
||||
///
|
||||
/// USB Mount Blocking. Defaults to false.
|
||||
///
|
||||
@@ -512,6 +522,12 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableBackwardsCompatibleContentEncoding;
|
||||
|
||||
///
|
||||
/// If set, "santactl sync" will use the supplied "Content-Encoding", possible
|
||||
/// settings include "gzip", "deflate", "none". If empty defaults to "deflate".
|
||||
///
|
||||
@property(readonly, nonatomic) SNTSyncContentEncoding syncClientContentEncoding;
|
||||
|
||||
///
|
||||
/// Contains the FCM project name.
|
||||
///
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
@@ -72,6 +73,7 @@ static NSString *const kMachineIDPlistFileKey = @"MachineIDPlist";
|
||||
static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
|
||||
static NSString *const kEnableSilentModeKey = @"EnableSilentMode";
|
||||
static NSString *const kEnableSilentTTYModeKey = @"EnableSilentTTYMode";
|
||||
static NSString *const kAboutTextKey = @"AboutText";
|
||||
static NSString *const kMoreInfoURLKey = @"MoreInfoURL";
|
||||
static NSString *const kEventDetailURLKey = @"EventDetailURL";
|
||||
@@ -107,8 +109,7 @@ static NSString *const kEnableForkAndExitLogging = @"EnableForkAndExitLogging";
|
||||
static NSString *const kIgnoreOtherEndpointSecurityClients = @"IgnoreOtherEndpointSecurityClients";
|
||||
static NSString *const kEnableDebugLogging = @"EnableDebugLogging";
|
||||
|
||||
static NSString *const kEnableBackwardsCompatibleContentEncoding =
|
||||
@"EnableBackwardsCompatibleContentEncoding";
|
||||
static NSString *const kClientContentEncoding = @"SyncClientContentEncoding";
|
||||
|
||||
static NSString *const kFCMProject = @"FCMProject";
|
||||
static NSString *const kFCMEntity = @"FCMEntity";
|
||||
@@ -128,7 +129,6 @@ static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
|
||||
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
|
||||
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
|
||||
|
||||
// TODO(markowsky): move these to sync server only.
|
||||
static NSString *const kMetricFormat = @"MetricFormat";
|
||||
static NSString *const kMetricURL = @"MetricURL";
|
||||
static NSString *const kMetricExportInterval = @"MetricExportInterval";
|
||||
@@ -181,6 +181,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kEnablePageZeroProtectionKey : number,
|
||||
kEnableBadSignatureProtectionKey : number,
|
||||
kEnableSilentModeKey : number,
|
||||
kEnableSilentTTYModeKey : number,
|
||||
kAboutTextKey : string,
|
||||
kMoreInfoURLKey : string,
|
||||
kEventDetailURLKey : string,
|
||||
@@ -193,11 +194,13 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kModeNotificationLockdown : string,
|
||||
kStaticRules : array,
|
||||
kSyncBaseURLKey : string,
|
||||
kSyncEnableCleanSyncEventUpload : number,
|
||||
kSyncProxyConfigKey : dictionary,
|
||||
kClientAuthCertificateFileKey : string,
|
||||
kClientAuthCertificatePasswordKey : string,
|
||||
kClientAuthCertificateCNKey : string,
|
||||
kClientAuthCertificateIssuerKey : string,
|
||||
kClientContentEncoding : string,
|
||||
kServerAuthRootsDataKey : data,
|
||||
kServerAuthRootsFileKey : string,
|
||||
kMachineOwnerKey : string,
|
||||
@@ -219,7 +222,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kEnableForkAndExitLogging : number,
|
||||
kIgnoreOtherEndpointSecurityClients : number,
|
||||
kEnableDebugLogging : number,
|
||||
kEnableBackwardsCompatibleContentEncoding : number,
|
||||
kFCMProject : string,
|
||||
kFCMEntity : string,
|
||||
kFCMAPIKey : string,
|
||||
@@ -313,6 +315,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableCleanSyncEventUpload {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnablePageZeroProtection {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -461,10 +467,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableBackwardsCompatibleContentEncoding {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFcmProject {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -645,6 +647,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (BOOL)enableSilentTTYMode {
|
||||
NSNumber *number = self.configState[kEnableSilentTTYModeKey];
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (NSString *)aboutText {
|
||||
return self.configState[kAboutTextKey];
|
||||
}
|
||||
@@ -708,6 +715,20 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return self.configState[kClientAuthCertificateIssuerKey];
|
||||
}
|
||||
|
||||
- (SNTSyncContentEncoding)syncClientContentEncoding {
|
||||
NSString *contentEncoding = [self.configState[kClientContentEncoding] lowercaseString];
|
||||
if ([contentEncoding isEqualToString:@"deflate"]) {
|
||||
return SNTSyncContentEncodingDeflate;
|
||||
} else if ([contentEncoding isEqualToString:@"gzip"]) {
|
||||
return SNTSyncContentEncodingGzip;
|
||||
} else if ([contentEncoding isEqualToString:@"none"]) {
|
||||
return SNTSyncContentEncodingNone;
|
||||
} else {
|
||||
// Ensure we have the same default zlib behavior Santa's always had otherwise.
|
||||
return SNTSyncContentEncodingDeflate;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSData *)syncServerAuthRootsData {
|
||||
return self.configState[kServerAuthRootsDataKey];
|
||||
}
|
||||
@@ -778,6 +799,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 {
|
||||
@@ -881,11 +904,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [number boolValue] || self.debugFlag;
|
||||
}
|
||||
|
||||
- (BOOL)enableBackwardsCompatibleContentEncoding {
|
||||
NSNumber *number = self.configState[kEnableBackwardsCompatibleContentEncoding];
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (NSString *)fcmProject {
|
||||
return self.configState[kFCMProject];
|
||||
}
|
||||
|
||||
83
Source/common/SNTFileAccessEvent.h
Normal file
83
Source/common/SNTFileAccessEvent.h
Normal 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
|
||||
79
Source/common/SNTFileAccessEvent.m
Normal file
79
Source/common/SNTFileAccessEvent.m
Normal 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
|
||||
@@ -100,6 +100,8 @@
|
||||
type = SNTRuleTypeCertificate;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeTeamID]) {
|
||||
type = SNTRuleTypeTeamID;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeSigningID]) {
|
||||
type = SNTRuleTypeSigningID;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,6 +122,7 @@ 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 kCursor;
|
||||
|
||||
|
||||
@@ -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,6 +123,7 @@ 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 kCursor = @"cursor";
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTRule *))reply;
|
||||
|
||||
///
|
||||
|
||||
@@ -17,13 +17,16 @@
|
||||
#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)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
|
||||
|
||||
@@ -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;
|
||||
|
||||
///
|
||||
|
||||
43
Source/common/String.h
Normal file
43
Source/common/String.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/// 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.
|
||||
|
||||
#ifndef SANTA__COMMON__STRING_H
|
||||
#define SANTA__COMMON__STRING_H
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace santa::common {
|
||||
|
||||
static inline std::string_view NSStringToUTF8StringView(NSString *str) {
|
||||
return std::string_view(str.UTF8String, [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -38,9 +38,12 @@
|
||||
// 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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -37,7 +51,7 @@ struct SNTDeviceMessageWindowView: View {
|
||||
Text("Device Name").bold()
|
||||
Text("Device BSD Path").bold()
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
if event!.remountArgs?.count ?? 0 > 0 {
|
||||
Text("Remount Mode").bold()
|
||||
}
|
||||
}
|
||||
@@ -46,7 +60,7 @@ struct SNTDeviceMessageWindowView: View {
|
||||
Text(event!.mntonname)
|
||||
Text(event!.mntfromname)
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
if event!.remountArgs?.count ?? 0 > 0 {
|
||||
Text(event!.readableRemountArgs())
|
||||
}
|
||||
}
|
||||
|
||||
35
Source/gui/SNTFileAccessMessageWindowController.h
Normal file
35
Source/gui/SNTFileAccessMessageWindowController.h
Normal 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
|
||||
79
Source/gui/SNTFileAccessMessageWindowController.m
Normal file
79
Source/gui/SNTFileAccessMessageWindowController.m
Normal 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
|
||||
158
Source/gui/SNTFileAccessMessageWindowView.swift
Normal file
158
Source/gui/SNTFileAccessMessageWindowView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
///
|
||||
|
||||
@@ -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 ()
|
||||
@@ -342,6 +345,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
|
||||
|
||||
@@ -37,14 +37,14 @@ objc_library(
|
||||
objc_library(
|
||||
name = "santactl_lib",
|
||||
srcs = [
|
||||
"main.m",
|
||||
"Commands/SNTCommandFileInfo.m",
|
||||
"Commands/SNTCommandRule.m",
|
||||
"Commands/SNTCommandStatus.m",
|
||||
"Commands/SNTCommandVersion.m",
|
||||
"Commands/SNTCommandMetrics.h",
|
||||
"Commands/SNTCommandMetrics.m",
|
||||
"Commands/SNTCommandRule.m",
|
||||
"Commands/SNTCommandStatus.m",
|
||||
"Commands/SNTCommandSync.m",
|
||||
"Commands/SNTCommandVersion.m",
|
||||
"main.m",
|
||||
] + select({
|
||||
"//:opt_build": [],
|
||||
"//conditions:default": [
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -49,6 +49,7 @@ objc_library(
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
],
|
||||
)
|
||||
@@ -208,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"],
|
||||
@@ -220,6 +231,7 @@ objc_library(
|
||||
":SNTPolicyProcessor",
|
||||
":SNTRuleTable",
|
||||
":SNTSyncdQueue",
|
||||
":TTYWriter",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:SNTBlockMessage",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
@@ -232,6 +244,7 @@ objc_library(
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:String",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
)
|
||||
@@ -284,7 +297,9 @@ objc_library(
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
],
|
||||
)
|
||||
@@ -330,6 +345,9 @@ objc_library(
|
||||
name = "SNTEndpointSecurityFileAccessAuthorizer",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.mm"],
|
||||
hdrs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.h"],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityAPI",
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
@@ -346,11 +364,13 @@ objc_library(
|
||||
"//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",
|
||||
],
|
||||
@@ -382,6 +402,7 @@ objc_library(
|
||||
":EndpointSecurityClient",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
@@ -468,6 +489,7 @@ objc_library(
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
"//Source/common:String",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -482,7 +504,6 @@ objc_library(
|
||||
":EndpointSecuritySerializerUtilities",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
],
|
||||
@@ -501,6 +522,7 @@ objc_library(
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:String",
|
||||
"//Source/common:santa_cc_proto_library_wrapper",
|
||||
],
|
||||
)
|
||||
@@ -569,6 +591,7 @@ objc_library(
|
||||
":EndpointSecurityWriterNull",
|
||||
":EndpointSecurityWriterSpool",
|
||||
":EndpointSecurityWriterSyslog",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
@@ -673,6 +696,7 @@ objc_library(
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTFileAccessEvent",
|
||||
"//Source/common:SNTKVOManager",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTXPCNotifierInterface",
|
||||
@@ -694,11 +718,13 @@ objc_library(
|
||||
":Metrics",
|
||||
":SNTCompilerController",
|
||||
":SNTDatabaseController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEventTable",
|
||||
":SNTExecutionController",
|
||||
":SNTNotificationQueue",
|
||||
":SNTRuleTable",
|
||||
":SNTSyncdQueue",
|
||||
":TTYWriter",
|
||||
":WatchItems",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -1251,6 +1277,7 @@ santa_unit_test(
|
||||
":SNTCompilerController",
|
||||
":SNTEndpointSecurityRecorder",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"//Source/common:Unit",
|
||||
"@OCMock",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
static const uint32_t kRuleTableCurrentVersion = 5;
|
||||
|
||||
// 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,25 @@ 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;
|
||||
}
|
||||
|
||||
// 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 +249,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,11 +284,11 @@ 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 {
|
||||
@@ -281,6 +300,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
|
||||
signingID:(NSString *)signingID
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID {
|
||||
__block SNTRule *rule;
|
||||
@@ -288,12 +308,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 +336,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 +351,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];
|
||||
}
|
||||
|
||||
@@ -43,6 +43,19 @@
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleSigningIDRuleIsPlatform:(BOOL)isPlatformBinary {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
if (isPlatformBinary) {
|
||||
r.identifier = @"platform:signingID";
|
||||
} else {
|
||||
r.identifier = @"teamID:signingID";
|
||||
}
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeSigningID;
|
||||
r.customMsg = @"A teamID 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,26 +184,66 @@
|
||||
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:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID");
|
||||
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:@"teamID:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID: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:@"teamID:signingID"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
@@ -196,6 +253,7 @@
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"teamID:signingID"
|
||||
certificateSHA256:@"unknowncert"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
@@ -205,12 +263,29 @@
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"teamID:signingID"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID, @"Implicit rule ordering failed (SigningID)");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID, @"Implicit rule ordering failed (TeamID)");
|
||||
}
|
||||
|
||||
- (void)testBadDatabase {
|
||||
|
||||
@@ -33,6 +33,7 @@ static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType =
|
||||
WatchItemPathType::kLiteral;
|
||||
static constexpr bool kWatchItemPolicyDefaultAllowReadAccess = false;
|
||||
static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
|
||||
static constexpr bool kWatchItemPolicyDefaultInvertProcessExceptions = false;
|
||||
|
||||
struct WatchItemPolicy {
|
||||
struct Process {
|
||||
@@ -69,19 +70,23 @@ struct WatchItemPolicy {
|
||||
WatchItemPathType pt = kWatchItemPolicyDefaultPathType,
|
||||
bool ara = kWatchItemPolicyDefaultAllowReadAccess,
|
||||
bool ao = kWatchItemPolicyDefaultAuditOnly,
|
||||
bool ipe = kWatchItemPolicyDefaultInvertProcessExceptions,
|
||||
std::vector<Process> procs = {})
|
||||
: name(n),
|
||||
path(p),
|
||||
path_type(pt),
|
||||
allow_read_access(ara),
|
||||
audit_only(ao),
|
||||
invert_process_exceptions(ipe),
|
||||
processes(std::move(procs)) {}
|
||||
|
||||
bool operator==(const WatchItemPolicy &other) const {
|
||||
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 &&
|
||||
processes == other.processes;
|
||||
}
|
||||
|
||||
bool operator!=(const WatchItemPolicy &other) const {
|
||||
@@ -93,7 +98,12 @@ struct WatchItemPolicy {
|
||||
WatchItemPathType path_type;
|
||||
bool allow_read_access;
|
||||
bool audit_only;
|
||||
bool invert_process_exceptions;
|
||||
std::vector<Process> processes;
|
||||
|
||||
// WIP - No current way to control via config
|
||||
bool silent = true;
|
||||
std::string version = "temp_version";
|
||||
};
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
|
||||
@@ -39,6 +39,7 @@ extern NSString *const kWatchItemConfigKeyPathsIsPrefix;
|
||||
extern NSString *const kWatchItemConfigKeyOptions;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsAllowReadAccess;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsAuditOnly;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions;
|
||||
extern NSString *const kWatchItemConfigKeyProcesses;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesBinaryPath;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesCertificateSha256;
|
||||
|
||||
@@ -34,8 +34,12 @@
|
||||
|
||||
#import "Source/common/PrefixTree.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/String.h"
|
||||
#import "Source/common/Unit.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
|
||||
using santa::common::NSStringToUTF8String;
|
||||
using santa::common::NSStringToUTF8StringView;
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
@@ -49,6 +53,7 @@ NSString *const kWatchItemConfigKeyPathsIsPrefix = @"IsPrefix";
|
||||
NSString *const kWatchItemConfigKeyOptions = @"Options";
|
||||
NSString *const kWatchItemConfigKeyOptionsAllowReadAccess = @"AllowReadAccess";
|
||||
NSString *const kWatchItemConfigKeyOptionsAuditOnly = @"AuditOnly";
|
||||
NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions = @"InvertProcessExceptions";
|
||||
NSString *const kWatchItemConfigKeyProcesses = @"Processes";
|
||||
NSString *const kWatchItemConfigKeyProcessesBinaryPath = @"BinaryPath";
|
||||
NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha256";
|
||||
@@ -255,7 +260,7 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
path_list.push_back({std::string(path_str.UTF8String, path_str.length), path_type});
|
||||
path_list.push_back({NSStringToUTF8String(path_str), path_type});
|
||||
} else if ([path isKindOfClass:[NSString class]]) {
|
||||
if (!LenRangeValidator(1, PATH_MAX)(path, err)) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Invalid path length: %@",
|
||||
@@ -264,8 +269,8 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
path_list.push_back({std::string(((NSString *)path).UTF8String, ((NSString *)path).length),
|
||||
kWatchItemPolicyDefaultPathType});
|
||||
path_list.push_back(
|
||||
{NSStringToUTF8String(((NSString *)path)), kWatchItemPolicyDefaultPathType});
|
||||
} else {
|
||||
PopulateError(
|
||||
err, [NSString stringWithFormat:
|
||||
@@ -340,12 +345,11 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
}
|
||||
|
||||
proc_list.push_back(WatchItemPolicy::Process(
|
||||
std::string([(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @"") UTF8String]),
|
||||
std::string([(process[kWatchItemConfigKeyProcessesSigningID] ?: @"") UTF8String]),
|
||||
std::string([(process[kWatchItemConfigKeyProcessesTeamID] ?: @"") UTF8String]),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @""),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesSigningID] ?: @""),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesTeamID] ?: @""),
|
||||
HexStringToBytes(process[kWatchItemConfigKeyProcessesCDHash]),
|
||||
std::string(
|
||||
[(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @"") UTF8String]),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @""),
|
||||
process[kWatchItemConfigKeyProcessesPlatformBinary]
|
||||
? std::make_optional(
|
||||
(bool)[process[kWatchItemConfigKeyProcessesPlatformBinary] boolValue])
|
||||
@@ -373,6 +377,8 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
/// <false/>
|
||||
/// <key>AuditOnly</key>
|
||||
/// <false/>
|
||||
/// <key>InvertProcessExceptions</key>
|
||||
/// <false/>
|
||||
/// </dict>
|
||||
/// <key>Processes</key>
|
||||
/// <array>
|
||||
@@ -407,6 +413,11 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAuditOnly, [NSNumber class], err)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsInvertProcessExceptions,
|
||||
[NSNumber class], err)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool allow_read_access = options[kWatchItemConfigKeyOptionsAllowReadAccess]
|
||||
@@ -415,6 +426,10 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
bool audit_only = options[kWatchItemConfigKeyOptionsAuditOnly]
|
||||
? [options[kWatchItemConfigKeyOptionsAuditOnly] boolValue]
|
||||
: kWatchItemPolicyDefaultAuditOnly;
|
||||
bool invert_process_exceptions =
|
||||
options[kWatchItemConfigKeyOptionsInvertProcessExceptions]
|
||||
? [options[kWatchItemConfigKeyOptionsInvertProcessExceptions] boolValue]
|
||||
: kWatchItemPolicyDefaultInvertProcessExceptions;
|
||||
|
||||
std::variant<Unit, ProcessList> proc_list = VerifyConfigWatchItemProcesses(watch_item, err);
|
||||
if (std::holds_alternative<Unit>(proc_list)) {
|
||||
@@ -423,8 +438,8 @@ 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>(
|
||||
[name UTF8String], path_type_pair.first, path_type_pair.second, allow_read_access, audit_only,
|
||||
std::get<ProcessList>(proc_list)));
|
||||
NSStringToUTF8StringView(name), path_type_pair.first, path_type_pair.second,
|
||||
allow_read_access, audit_only, invert_process_exceptions, std::get<ProcessList>(proc_list)));
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -645,7 +660,7 @@ void WatchItems::UpdateCurrentState(
|
||||
std::swap(currently_monitored_paths_, new_monitored_paths);
|
||||
current_config_ = new_config;
|
||||
if (new_config) {
|
||||
policy_version_ = [new_config[kWatchItemConfigKeyVersion] UTF8String];
|
||||
policy_version_ = NSStringToUTF8String(new_config[kWatchItemConfigKeyVersion]);
|
||||
} else {
|
||||
policy_version_ = "";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -777,6 +778,17 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
},
|
||||
policies, &err));
|
||||
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsInvertProcessExceptions : @""}
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsInvertProcessExceptions : @(0)}
|
||||
},
|
||||
policies, &err));
|
||||
|
||||
// If processes are specified, they must be valid format
|
||||
// Note: Full tests in `testVerifyConfigWatchItemProcesses`
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(
|
||||
@@ -790,9 +802,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 +820,8 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
@[ @"a", @{kWatchItemConfigKeyPathsPath : @"b", kWatchItemConfigKeyPathsIsPrefix : @(YES)} ],
|
||||
kWatchItemConfigKeyOptions : @{
|
||||
kWatchItemConfigKeyOptionsAllowReadAccess : @(YES),
|
||||
kWatchItemConfigKeyOptionsAuditOnly : @(NO)
|
||||
kWatchItemConfigKeyOptionsAuditOnly : @(NO),
|
||||
kWatchItemConfigKeyOptionsInvertProcessExceptions : @(YES),
|
||||
},
|
||||
kWatchItemConfigKeyProcesses : @[
|
||||
@{kWatchItemConfigKeyProcessesBinaryPath : @"pa"},
|
||||
@@ -815,10 +830,10 @@ 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, procs));
|
||||
XCTAssertEqual(*policies[1].get(), WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true,
|
||||
false, true, procs));
|
||||
}
|
||||
|
||||
- (void)testState {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
@@ -33,16 +34,29 @@ enum class FlushCacheMode {
|
||||
kAllCaches,
|
||||
};
|
||||
|
||||
enum class FlushCacheReason {
|
||||
kClientModeChanged,
|
||||
kPathRegexChanged,
|
||||
kRulesChanged,
|
||||
kStaticRulesChanged,
|
||||
kExplicitCommand,
|
||||
kFilesystemUnmounted,
|
||||
};
|
||||
|
||||
class AuthResultCache {
|
||||
public:
|
||||
// Santa currently only flushes caches when new DENY rules are added, not
|
||||
// ALLOW rules. This means this value should be low enough so that if a
|
||||
// ALLOW rules. This means cache_deny_time_ms should be low enough so that if a
|
||||
// previously denied binary is allowed, it can be re-executed by the user in a
|
||||
// timely manner. But the value should be high enough to allow the cache to be
|
||||
// effective in the event the binary is executed in rapid succession.
|
||||
static std::unique_ptr<AuthResultCache> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTMetricSet *metric_set, uint64_t cache_deny_time_ms = 1500);
|
||||
|
||||
AuthResultCache(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms = 1500);
|
||||
SNTMetricCounter *flush_count, uint64_t cache_deny_time_ms = 1500);
|
||||
virtual ~AuthResultCache();
|
||||
|
||||
AuthResultCache(AuthResultCache &&other) = delete;
|
||||
@@ -55,7 +69,7 @@ class AuthResultCache {
|
||||
virtual SNTAction CheckCache(const es_file_t *es_file);
|
||||
virtual SNTAction CheckCache(SantaVnode vnode_id);
|
||||
|
||||
virtual void FlushCache(FlushCacheMode mode);
|
||||
virtual void FlushCache(FlushCacheMode mode, FlushCacheReason reason);
|
||||
|
||||
virtual NSArray<NSNumber *> *CacheCounts();
|
||||
|
||||
@@ -66,6 +80,7 @@ class AuthResultCache {
|
||||
SantaCache<SantaVnode, uint64_t> *nonroot_cache_;
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
SNTMetricCounter *flush_count_;
|
||||
uint64_t root_devno_;
|
||||
uint64_t cache_deny_time_ns_;
|
||||
dispatch_queue_t q_;
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
|
||||
static NSString *const kFlushCacheReasonClientModeChanged = @"ClientModeChanged";
|
||||
static NSString *const kFlushCacheReasonPathRegexChanged = @"PathRegexChanged";
|
||||
static NSString *const kFlushCacheReasonRulesChanged = @"RulesChanged";
|
||||
static NSString *const kFlushCacheReasonStaticRulesChanged = @"StaticRulesChanged";
|
||||
static NSString *const kFlushCacheReasonExplicitCommand = @"ExplicitCommand";
|
||||
static NSString *const kFlushCacheReasonFilesystemUnmounted = @"FilesystemUnmounted";
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
static inline uint64_t GetCurrentUptime() {
|
||||
@@ -44,9 +51,36 @@ static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
|
||||
return (cachedValue & ~(0xFF00000000000000));
|
||||
}
|
||||
|
||||
NSString *const FlushCacheReasonToString(FlushCacheReason reason) {
|
||||
switch (reason) {
|
||||
case FlushCacheReason::kClientModeChanged: return kFlushCacheReasonClientModeChanged;
|
||||
case FlushCacheReason::kPathRegexChanged: return kFlushCacheReasonPathRegexChanged;
|
||||
case FlushCacheReason::kRulesChanged: return kFlushCacheReasonRulesChanged;
|
||||
case FlushCacheReason::kStaticRulesChanged: return kFlushCacheReasonStaticRulesChanged;
|
||||
case FlushCacheReason::kExplicitCommand: return kFlushCacheReasonExplicitCommand;
|
||||
case FlushCacheReason::kFilesystemUnmounted: return kFlushCacheReasonFilesystemUnmounted;
|
||||
default:
|
||||
[NSException raise:@"Invalid reason" format:@"Unknown reason value: %d", reason];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<AuthResultCache> AuthResultCache::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTMetricSet *metric_set,
|
||||
uint64_t cache_deny_time_ms) {
|
||||
SNTMetricCounter *flush_count =
|
||||
[metric_set counterWithName:@"/santa/flush_count"
|
||||
fieldNames:@[ @"Reason" ]
|
||||
helpText:@"Count of times the auth result cache is flushed by reason"];
|
||||
|
||||
return std::make_unique<AuthResultCache>(esapi, flush_count, cache_deny_time_ms);
|
||||
}
|
||||
|
||||
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
SNTMetricCounter *flush_count, uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi),
|
||||
flush_count_(flush_count),
|
||||
cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
root_cache_ = new SantaCache<SantaVnode, uint64_t>();
|
||||
nonroot_cache_ = new SantaCache<SantaVnode, uint64_t>();
|
||||
|
||||
@@ -118,7 +152,7 @@ SantaCache<SantaVnode, uint64_t> *AuthResultCache::CacheForVnodeID(SantaVnode vn
|
||||
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
|
||||
}
|
||||
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode, FlushCacheReason reason) {
|
||||
nonroot_cache_->clear();
|
||||
if (mode == FlushCacheMode::kAllCaches) {
|
||||
root_cache_->clear();
|
||||
@@ -134,6 +168,8 @@ void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
shared_esapi->ClearCache(Client());
|
||||
});
|
||||
}
|
||||
|
||||
[flush_count_ incrementForFieldValues:@[ FlushCacheReasonToString(reason) ]];
|
||||
}
|
||||
|
||||
NSArray<NSNumber *> *AuthResultCache::CacheCounts() {
|
||||
|
||||
@@ -29,6 +29,13 @@
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
extern NSString *const FlushCacheReasonToString(FlushCacheReason reason);
|
||||
}
|
||||
|
||||
using santa::santad::event_providers::FlushCacheReasonToString;
|
||||
|
||||
// Grab the st_dev number of the root volume to match the root cache
|
||||
static uint64_t RootDevno() {
|
||||
@@ -66,14 +73,14 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testEmptyCacheExpectedNumberOfCacheCounts {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(esapi, nil);
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testBasicOperation {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(esapi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
|
||||
@@ -110,7 +117,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testFlushCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
|
||||
@@ -121,7 +128,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush non-root only
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly, FlushCacheReason::kClientModeChanged);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
@@ -138,7 +145,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
dispatch_semaphore_signal(sema);
|
||||
return true;
|
||||
}));
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches, FlushCacheReason::kClientModeChanged);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
@@ -151,7 +158,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testCacheStateMachine {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
@@ -193,7 +200,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
// Create a cache with a lowered cache expiry value
|
||||
uint64_t expiryMS = 250;
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi, expiryMS);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil, expiryMS);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
@@ -215,4 +222,21 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testFlushCacheReasonToString {
|
||||
std::map<FlushCacheReason, NSString *> reasonToString = {
|
||||
{FlushCacheReason::kClientModeChanged, @"ClientModeChanged"},
|
||||
{FlushCacheReason::kPathRegexChanged, @"PathRegexChanged"},
|
||||
{FlushCacheReason::kRulesChanged, @"RulesChanged"},
|
||||
{FlushCacheReason::kStaticRulesChanged, @"StaticRulesChanged"},
|
||||
{FlushCacheReason::kExplicitCommand, @"ExplicitCommand"},
|
||||
{FlushCacheReason::kFilesystemUnmounted, @"FilesystemUnmounted"},
|
||||
};
|
||||
|
||||
for (const auto &kv : reasonToString) {
|
||||
XCTAssertEqualObjects(FlushCacheReasonToString(kv.first), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(FlushCacheReasonToString((FlushCacheReason)12345));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -234,7 +234,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, CheckCache)
|
||||
.WillOnce(testing::Return(SNTActionRequestBinary))
|
||||
.WillOnce(testing::Return(SNTActionRequestBinary))
|
||||
@@ -301,7 +301,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllowCompiler))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllow))
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,21 @@ 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
|
||||
// after signaling, but _before_ the implicit release
|
||||
// of msg. In that case, the mock verify and the
|
||||
// call of the mock's Release method can data race.
|
||||
msg.reset();
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
@@ -172,6 +173,15 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
|
||||
// Process the unmount event first so that caches are flushed before any
|
||||
// other potential early returns.
|
||||
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly,
|
||||
FlushCacheReason::kFilesystemUnmounted);
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.blockUSBMount) {
|
||||
// TODO: We should also unsubscribe from events when this isn't set, but
|
||||
// this is generally a low-volume event type.
|
||||
@@ -180,12 +190,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return;
|
||||
}
|
||||
|
||||
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
return;
|
||||
}
|
||||
|
||||
[self processMessage:std::move(esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
es_auth_result_t result = [self handleAuthMount:msg];
|
||||
|
||||
@@ -39,13 +39,14 @@
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
class MockAuthResultCache : public AuthResultCache {
|
||||
public:
|
||||
using AuthResultCache::AuthResultCache;
|
||||
|
||||
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode));
|
||||
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode, FlushCacheReason reason));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager (Testing)
|
||||
@@ -316,7 +317,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, FlushCache);
|
||||
|
||||
SNTEndpointSecurityDeviceManager *deviceManager =
|
||||
|
||||
@@ -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"
|
||||
@@ -25,6 +26,8 @@
|
||||
#include "Source/santad/Metrics.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
typedef void (^SNTFileAccessBlockCallback)(SNTFileAccessEvent *event);
|
||||
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizer
|
||||
: SNTEndpointSecurityClient <SNTEndpointSecurityDynamicEventHandler>
|
||||
|
||||
@@ -38,4 +41,6 @@
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
|
||||
decisionCache:(SNTDecisionCache *)decisionCache;
|
||||
|
||||
@property SNTFileAccessBlockCallback fileAccessBlockCallback;
|
||||
|
||||
@end
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <sys/fcntl.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -32,17 +33,20 @@
|
||||
#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"
|
||||
|
||||
using santa::common::StringToNSString;
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
@@ -241,7 +245,6 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
[self establishClientOrDie];
|
||||
|
||||
[super enableTargetPathWatching];
|
||||
[super unmuteEverything];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -426,19 +429,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 +485,27 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
targetPathCopy, policyDecision);
|
||||
}];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return policyDecision;
|
||||
@@ -554,7 +594,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
if ([super unsubscribeAll]) {
|
||||
self.isSubscribed = false;
|
||||
}
|
||||
[super unmuteEverything];
|
||||
[super unmuteAllTargetPaths];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ extern es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_r
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -532,8 +531,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 +546,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 +558,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 +576,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 +588,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);
|
||||
}
|
||||
|
||||
@@ -636,7 +681,6 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
decisionCache:nil];
|
||||
|
||||
EXPECT_CALL(*mockESApi, UnsubscribeAll);
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
|
||||
|
||||
accessClient.isSubscribed = true;
|
||||
|
||||
@@ -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);
|
||||
}];
|
||||
|
||||
@@ -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);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFile)).Times(1);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
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);
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,19 +17,22 @@
|
||||
#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>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#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"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
@@ -37,7 +40,10 @@
|
||||
|
||||
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;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
|
||||
@@ -63,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);
|
||||
@@ -94,15 +103,15 @@ static inline void EncodePath(std::string *buf, const es_file_t *es_file) {
|
||||
buf->append(std::string_view(es_file->path.data, es_file->path.length));
|
||||
}
|
||||
|
||||
static inline void EncodeString(std::string *buf, NSString *value) {
|
||||
static inline void EncodeString(std::function<std::string *()> lazy_f, NSString *value) {
|
||||
if (value) {
|
||||
buf->append(std::string_view([value UTF8String], [value length]));
|
||||
lazy_f()->append(NSStringToUTF8StringView(value));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeString(std::string *buf, std::string_view value) {
|
||||
static inline void EncodeString(std::function<std::string *()> lazy_f, std::string_view value) {
|
||||
if (value.length() > 0) {
|
||||
buf->append(std::string_view(value.data(), value.length()));
|
||||
lazy_f()->append(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +134,7 @@ static inline void EncodeGroupInfo(::pbv1::GroupInfo *pb_group_info, gid_t gid,
|
||||
static inline void EncodeHash(::pbv1::Hash *pb_hash, NSString *sha256) {
|
||||
if (sha256) {
|
||||
pb_hash->set_type(::pbv1::Hash::HASH_ALGO_SHA256);
|
||||
pb_hash->set_hash([sha256 UTF8String], [sha256 length]);
|
||||
EncodeString([pb_hash] { return pb_hash->mutable_hash(); }, sha256);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +171,7 @@ static inline void EncodeFileInfo(::pbv1::FileInfo *pb_file, const es_file_t *es
|
||||
|
||||
static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, std::string_view path,
|
||||
bool truncated) {
|
||||
EncodeString(pb_file->mutable_path(), path);
|
||||
EncodeString([pb_file] { return pb_file->mutable_path(); }, path);
|
||||
pb_file->set_truncated(truncated);
|
||||
}
|
||||
|
||||
@@ -262,9 +271,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
EncodeHash(pb_cert_info->mutable_hash(), cert_hash);
|
||||
}
|
||||
|
||||
if (common_name) {
|
||||
pb_cert_info->set_common_name([common_name UTF8String], [common_name length]);
|
||||
}
|
||||
EncodeString([pb_cert_info] { return pb_cert_info->mutable_common_name(); }, common_name);
|
||||
}
|
||||
|
||||
::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state) {
|
||||
@@ -286,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;
|
||||
@@ -356,7 +365,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
|
||||
|
||||
if (EnabledMachineID()) {
|
||||
EncodeString(santa_msg->mutable_machine_id(), MachineID());
|
||||
EncodeString([santa_msg] { return santa_msg->mutable_machine_id(); }, MachineID());
|
||||
}
|
||||
EncodeTimestamp(santa_msg->mutable_event_time(), event_time);
|
||||
EncodeTimestamp(santa_msg->mutable_processed_time(), processed_time);
|
||||
@@ -383,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;
|
||||
@@ -422,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();
|
||||
@@ -485,24 +511,17 @@ 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);
|
||||
}
|
||||
|
||||
if (cd.decisionExtra) {
|
||||
pb_exec->set_explain([cd.decisionExtra UTF8String], [cd.decisionExtra length]);
|
||||
}
|
||||
|
||||
if (cd.quarantineURL) {
|
||||
pb_exec->set_quarantine_url([cd.quarantineURL UTF8String], [cd.quarantineURL length]);
|
||||
}
|
||||
EncodeString([pb_exec] { return pb_exec->mutable_explain(); }, cd.decisionExtra);
|
||||
EncodeString([pb_exec] { return pb_exec->mutable_quarantine_url(); }, cd.quarantineURL);
|
||||
|
||||
NSString *orig_path = Utilities::OriginalPathForTranslocation(msg.es_msg().event.exec.target);
|
||||
if (orig_path) {
|
||||
pb_exec->set_original_path([orig_path UTF8String], [orig_path length]);
|
||||
}
|
||||
EncodeString([pb_exec] { return pb_exec->mutable_original_path(); }, orig_path);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
@@ -594,8 +613,9 @@ std::vector<uint8_t> Protobuf::SerializeFileAccess(const std::string &policy_ver
|
||||
EncodeProcessInfo(file_access->mutable_instigator(), msg->version, msg->process,
|
||||
enriched_process);
|
||||
EncodeFileInfoLight(file_access->mutable_target(), target, false);
|
||||
EncodeString(file_access->mutable_policy_version(), policy_version);
|
||||
EncodeString(file_access->mutable_policy_name(), policy_name);
|
||||
EncodeString([file_access] { return file_access->mutable_policy_version(); }, policy_version);
|
||||
EncodeString([file_access] { return file_access->mutable_policy_name(); }, policy_name);
|
||||
|
||||
file_access->set_access_type(GetAccessType(msg->event_type));
|
||||
file_access->set_policy_decision(GetPolicyDecision(decision));
|
||||
|
||||
@@ -629,10 +649,12 @@ std::vector<uint8_t> Protobuf::SerializeBundleHashingEvent(SNTStoredEvent *event
|
||||
|
||||
EncodeHash(pb_bundle->mutable_file_hash(), event.fileSHA256);
|
||||
EncodeHash(pb_bundle->mutable_bundle_hash(), event.fileBundleHash);
|
||||
pb_bundle->set_bundle_name([NonNull(event.fileBundleName) UTF8String]);
|
||||
pb_bundle->set_bundle_id([NonNull(event.fileBundleID) UTF8String]);
|
||||
pb_bundle->set_bundle_path([NonNull(event.fileBundlePath) UTF8String]);
|
||||
pb_bundle->set_path([NonNull(event.filePath) UTF8String]);
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_name(); },
|
||||
NonNull(event.fileBundleName));
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_id(); }, NonNull(event.fileBundleID));
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_path(); },
|
||||
NonNull(event.fileBundlePath));
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_path(); }, NonNull(event.filePath));
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
@@ -652,14 +674,14 @@ static void EncodeDisk(::pbv1::Disk *pb_disk, ::pbv1::Disk_Action action, NSDict
|
||||
stringWithFormat:@"%@ %@", NonNull(props[@"DADeviceVendor"]), NonNull(props[@"DADeviceModel"])];
|
||||
model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
EncodeString(pb_disk->mutable_mount(), [props[@"DAVolumePath"] path]);
|
||||
EncodeString(pb_disk->mutable_volume(), props[@"DAVolumeName"]);
|
||||
EncodeString(pb_disk->mutable_bsd_name(), props[@"DAMediaBSDName"]);
|
||||
EncodeString(pb_disk->mutable_fs(), props[@"DAVolumeKind"]);
|
||||
EncodeString(pb_disk->mutable_model(), model);
|
||||
EncodeString(pb_disk->mutable_serial(), serial);
|
||||
EncodeString(pb_disk->mutable_bus(), props[@"DADeviceProtocol"]);
|
||||
EncodeString(pb_disk->mutable_dmg_path(), dmg_path);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_mount(); }, [props[@"DAVolumePath"] path]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_volume(); }, props[@"DAVolumeName"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_bsd_name(); }, props[@"DAMediaBSDName"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_fs(); }, props[@"DAVolumeKind"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_model(); }, model);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_serial(); }, serial);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_bus(); }, props[@"DADeviceProtocol"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_dmg_path(); }, dmg_path);
|
||||
|
||||
if (props[@"DAAppearanceTime"]) {
|
||||
// Note: `DAAppearanceTime` is set via `CFAbsoluteTimeGetCurrent`, which uses the defined
|
||||
|
||||
@@ -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 ×tamp, 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;
|
||||
|
||||
@@ -51,8 +51,7 @@ class SanitizableString {
|
||||
friend std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string);
|
||||
|
||||
private:
|
||||
const char *data_;
|
||||
size_t length_;
|
||||
std::string_view data_;
|
||||
mutable bool sanitized_ = false;
|
||||
mutable std::optional<std::string> sanitized_string_;
|
||||
};
|
||||
|
||||
@@ -14,37 +14,35 @@
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
|
||||
|
||||
#include "Source/common/String.h"
|
||||
|
||||
using santa::common::NSStringToUTF8StringView;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
SanitizableString::SanitizableString(const es_file_t *file)
|
||||
: data_(file->path.data), length_(file->path.length) {}
|
||||
: data_(file->path.data, file->path.length) {}
|
||||
|
||||
SanitizableString::SanitizableString(const es_string_token_t &tok)
|
||||
: data_(tok.data), length_(tok.length) {}
|
||||
SanitizableString::SanitizableString(const es_string_token_t &tok) : data_(tok.data, tok.length) {}
|
||||
|
||||
SanitizableString::SanitizableString(NSString *str)
|
||||
: data_([str UTF8String]), length_([str length]) {}
|
||||
SanitizableString::SanitizableString(NSString *str) : data_(NSStringToUTF8StringView(str)) {}
|
||||
|
||||
SanitizableString::SanitizableString(const char *str, size_t len) : data_(str), length_(len) {}
|
||||
SanitizableString::SanitizableString(const char *str, size_t len) : data_(str, len) {}
|
||||
|
||||
std::string_view SanitizableString::String() const {
|
||||
return std::string_view(data_, length_);
|
||||
return data_;
|
||||
}
|
||||
|
||||
std::string_view SanitizableString::Sanitized() const {
|
||||
if (!sanitized_) {
|
||||
sanitized_ = true;
|
||||
sanitized_string_ = SanitizeString(data_, length_);
|
||||
sanitized_string_ = SanitizeString(data_.data(), data_.length());
|
||||
}
|
||||
|
||||
if (sanitized_string_.has_value()) {
|
||||
return sanitized_string_.value();
|
||||
} else {
|
||||
if (data_) {
|
||||
return std::string_view(data_, length_);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
return data_;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -56,7 +56,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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,6 +39,7 @@ using santa::santad::data_layer::WatchItems;
|
||||
using santa::santad::data_layer::WatchItemsState;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
// Globals used by the santad watchdog thread
|
||||
@@ -84,12 +85,9 @@ double watchdogRAMPeak = 0;
|
||||
reply([counts[0] unsignedLongLongValue], [counts[1] unsignedLongLongValue]);
|
||||
}
|
||||
|
||||
- (void)flushAllCaches {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
}
|
||||
|
||||
- (void)flushCache:(void (^)(BOOL))reply {
|
||||
[self flushAllCaches];
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kExplicitCommand);
|
||||
reply(YES);
|
||||
}
|
||||
|
||||
@@ -100,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
|
||||
@@ -125,7 +123,7 @@ double watchdogRAMPeak = 0;
|
||||
// The actual cache flushing happens after the new rules have been added to the database.
|
||||
if (flushCache) {
|
||||
LOGI(@"Flushing caches");
|
||||
[self flushAllCaches];
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches, FlushCacheReason::kRulesChanged);
|
||||
}
|
||||
|
||||
reply(error);
|
||||
@@ -146,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]);
|
||||
}
|
||||
@@ -162,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -36,6 +37,7 @@
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/common/SantaVnode.h"
|
||||
#include "Source/common/String.h"
|
||||
#import "Source/santad/DataLayer/SNTEventTable.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
@@ -43,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
|
||||
@@ -58,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/"
|
||||
@@ -73,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 =
|
||||
@@ -107,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;
|
||||
@@ -199,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);
|
||||
|
||||
@@ -236,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());
|
||||
@@ -293,7 +304,8 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
NSAttributedString *s = [SNTBlockMessage attributedBlockMessageForEvent:se
|
||||
customMessage:cd.customMsg];
|
||||
|
||||
if (targetProc->tty && targetProc->tty->path.length > 0) {
|
||||
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"
|
||||
@@ -305,7 +317,7 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
[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];
|
||||
@@ -358,12 +370,6 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
return proxyInfo;
|
||||
}
|
||||
|
||||
- (void)printMessage:(NSString *)msg toTTY:(const char *)path {
|
||||
int fd = open(path, O_WRONLY | O_NOCTTY);
|
||||
write(fd, msg.UTF8String, msg.length);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
- (void)loggedInUsers:(NSArray **)users sessions:(NSArray **)sessions {
|
||||
NSMutableSet *loggedInUsers = [NSMutableSet set];
|
||||
NSMutableArray *loggedInHosts = [NSMutableArray array];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
@@ -108,6 +128,19 @@
|
||||
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.decision = SNTEventStateBlockSigningID;
|
||||
return cd;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case SNTRuleTypeCertificate:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCertificate; return cd;
|
||||
@@ -161,22 +194,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 +242,8 @@
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID];
|
||||
teamID:teamID
|
||||
signingID:signingID];
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
@@ -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"
|
||||
@@ -43,6 +44,7 @@ using santa::santad::Metrics;
|
||||
using santa::santad::data_layer::WatchItems;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
@@ -129,46 +131,55 @@ 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]];
|
||||
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 = @[
|
||||
[[SNTKVOManager alloc] initWithObject:configurator
|
||||
selector:@selector(clientMode)
|
||||
type:[NSNumber class]
|
||||
callback:^(NSNumber *oldValue, NSNumber *newValue) {
|
||||
if ([oldValue longLongValue] == [newValue longLongValue]) {
|
||||
// Note: This case apparently can happen and if not checked
|
||||
// will result in excessive notification messages sent to the
|
||||
// user when calling `postClientModeNotification` below
|
||||
return;
|
||||
}
|
||||
NSMutableArray<SNTKVOManager *> *kvoObservers = [[NSMutableArray alloc] init];
|
||||
[kvoObservers addObjectsFromArray:@[
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(clientMode)
|
||||
type:[NSNumber class]
|
||||
callback:^(NSNumber *oldValue, NSNumber *newValue) {
|
||||
if ([oldValue longLongValue] == [newValue longLongValue]) {
|
||||
// Note: This case apparently can happen and if not checked
|
||||
// will result in excessive notification messages sent to the
|
||||
// user when calling `postClientModeNotification` below
|
||||
return;
|
||||
}
|
||||
|
||||
SNTClientMode clientMode =
|
||||
(SNTClientMode)[newValue longLongValue];
|
||||
SNTClientMode clientMode = (SNTClientMode)[newValue longLongValue];
|
||||
|
||||
switch (clientMode) {
|
||||
case SNTClientModeLockdown:
|
||||
LOGI(@"Changed client mode to Lockdown, flushing cache.");
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
break;
|
||||
case SNTClientModeMonitor:
|
||||
LOGI(@"Changed client mode to Monitor.");
|
||||
break;
|
||||
default: LOGW(@"Changed client mode to unknown value."); break;
|
||||
}
|
||||
switch (clientMode) {
|
||||
case SNTClientModeLockdown:
|
||||
LOGI(@"Changed client mode to Lockdown, flushing cache.");
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kClientModeChanged);
|
||||
break;
|
||||
case SNTClientModeMonitor: LOGI(@"Changed client mode to Monitor."); break;
|
||||
default: LOGW(@"Changed client mode to unknown value."); break;
|
||||
}
|
||||
|
||||
[[notifier_queue.notifierConnection remoteObjectProxy]
|
||||
postClientModeNotification:clientMode];
|
||||
}],
|
||||
[[notifier_queue.notifierConnection remoteObjectProxy]
|
||||
postClientModeNotification:clientMode];
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(syncBaseURL)
|
||||
@@ -233,7 +244,8 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
}
|
||||
|
||||
LOGI(@"Changed allowlist regex, flushing cache");
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kPathRegexChanged);
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
@@ -246,7 +258,8 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
}
|
||||
|
||||
LOGI(@"Changed denylist regex, flushing cache");
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kPathRegexChanged);
|
||||
}],
|
||||
[[SNTKVOManager alloc] initWithObject:configurator
|
||||
selector:@selector(blockUSBMount)
|
||||
@@ -292,39 +305,21 @@ 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);
|
||||
auth_result_cache->FlushCache(
|
||||
FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kStaticRulesChanged);
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
@@ -356,7 +351,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.
|
||||
|
||||
@@ -49,6 +49,8 @@ class SantadDeps {
|
||||
std::unique_ptr<santa::santad::logs::endpoint_security::Logger> logger,
|
||||
std::shared_ptr<santa::santad::Metrics> metrics,
|
||||
std::shared_ptr<santa::santad::data_layer::WatchItems> watch_items,
|
||||
std::shared_ptr<santa::santad::event_providers::AuthResultCache>
|
||||
auth_result_cache,
|
||||
MOLXPCConnection *control_connection,
|
||||
SNTCompilerController *compiler_controller,
|
||||
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "Source/santad/SantadDeps.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
@@ -24,10 +25,13 @@
|
||||
#include "Source/santad/DataLayer/WatchItems.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#import "Source/santad/SNTDatabaseController.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::endpoint_security::EndpointSecurityAPI;
|
||||
@@ -80,11 +84,17 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
std::shared_ptr<TTYWriter> tty_writer = TTYWriter::Create();
|
||||
if (!tty_writer) {
|
||||
LOGW(@"Unable to initialize TTY writer");
|
||||
}
|
||||
|
||||
SNTExecutionController *exec_controller =
|
||||
[[SNTExecutionController alloc] initWithRuleTable:rule_table
|
||||
eventTable:event_table
|
||||
notifierQueue:notifier_queue
|
||||
syncdQueue:syncd_queue];
|
||||
syncdQueue:syncd_queue
|
||||
ttyWriter:tty_writer];
|
||||
if (!exec_controller) {
|
||||
LOGE(@"Failed to initialize exec controller.");
|
||||
exit(EXIT_FAILURE);
|
||||
@@ -109,9 +119,10 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
|
||||
size_t spool_dir_threshold_bytes = [configurator spoolDirectorySizeThresholdMB] * 1024 * 1024;
|
||||
uint64_t spool_flush_timeout_ms = [configurator spoolDirectoryEventMaxFlushTimeSec] * 1000;
|
||||
|
||||
std::unique_ptr<::Logger> logger = Logger::Create(
|
||||
esapi, [configurator eventLogType], [configurator eventLogPath], [configurator spoolDirectory],
|
||||
spool_dir_threshold_bytes, spool_file_threshold_bytes, spool_flush_timeout_ms);
|
||||
std::unique_ptr<::Logger> logger =
|
||||
Logger::Create(esapi, [configurator eventLogType], [SNTDecisionCache sharedCache],
|
||||
[configurator eventLogPath], [configurator spoolDirectory],
|
||||
spool_dir_threshold_bytes, spool_file_threshold_bytes, spool_flush_timeout_ms);
|
||||
if (!logger) {
|
||||
LOGE(@"Failed to create logger.");
|
||||
exit(EXIT_FAILURE);
|
||||
@@ -135,25 +146,31 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
return std::make_unique<SantadDeps>(
|
||||
esapi, std::move(logger), std::move(metrics), std::move(watch_items), control_connection,
|
||||
compiler_controller, notifier_queue, syncd_queue, exec_controller, prefix_tree);
|
||||
std::shared_ptr<::AuthResultCache> auth_result_cache = AuthResultCache::Create(esapi, metric_set);
|
||||
if (!auth_result_cache) {
|
||||
LOGE(@"Failed to create auth result cache");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
return std::make_unique<SantadDeps>(esapi, std::move(logger), std::move(metrics),
|
||||
std::move(watch_items), std::move(auth_result_cache),
|
||||
control_connection, compiler_controller, notifier_queue,
|
||||
syncd_queue, exec_controller, prefix_tree);
|
||||
}
|
||||
|
||||
SantadDeps::SantadDeps(std::shared_ptr<EndpointSecurityAPI> esapi, std::unique_ptr<::Logger> logger,
|
||||
std::shared_ptr<::Metrics> metrics,
|
||||
std::shared_ptr<::WatchItems> watch_items,
|
||||
MOLXPCConnection *control_connection,
|
||||
SNTCompilerController *compiler_controller,
|
||||
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
|
||||
SNTExecutionController *exec_controller,
|
||||
std::shared_ptr<::PrefixTree<Unit>> prefix_tree)
|
||||
SantadDeps::SantadDeps(
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi, std::unique_ptr<::Logger> logger,
|
||||
std::shared_ptr<::Metrics> metrics, std::shared_ptr<::WatchItems> watch_items,
|
||||
std::shared_ptr<santa::santad::event_providers::AuthResultCache> auth_result_cache,
|
||||
MOLXPCConnection *control_connection, SNTCompilerController *compiler_controller,
|
||||
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
|
||||
SNTExecutionController *exec_controller, std::shared_ptr<::PrefixTree<Unit>> prefix_tree)
|
||||
: esapi_(std::move(esapi)),
|
||||
logger_(std::move(logger)),
|
||||
metrics_(std::move(metrics)),
|
||||
watch_items_(std::move(watch_items)),
|
||||
enricher_(std::make_shared<::Enricher>()),
|
||||
auth_result_cache_(std::make_shared<::AuthResultCache>(esapi_)),
|
||||
auth_result_cache_(std::move(auth_result_cache)),
|
||||
control_connection_(control_connection),
|
||||
compiler_controller_(compiler_controller),
|
||||
notifier_queue_(notifier_queue),
|
||||
|
||||
@@ -37,6 +37,11 @@ using santa::santad::SantadDeps;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
static const char *kAllowedSigningID = "com.google.allowed_signing_id";
|
||||
static const char *kBlockedSigningID = "com.google.blocked_signing_id";
|
||||
static const char *kNoRuleMatchSigningID = "com.google.no_rule_match_signing_id";
|
||||
static const char *kBlockedTeamID = "EQHXZ8M8AV";
|
||||
static const char *kNoRuleMatchTeamID = "ABC1234XYZ";
|
||||
|
||||
@interface SantadTest : XCTestCase
|
||||
@property id mockSNTDatabaseController;
|
||||
@@ -56,7 +61,8 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
|
||||
- (BOOL)checkBinaryExecution:(NSString *)binaryName
|
||||
wantResult:(es_auth_result_t)wantResult
|
||||
clientMode:(NSInteger)clientMode {
|
||||
clientMode:(NSInteger)clientMode
|
||||
messageSetup:(void (^)(es_message_t *))messageSetupBlock {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
|
||||
@@ -102,6 +108,7 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
lstat(binaryPath.UTF8String, &fileStat);
|
||||
es_file_t file = MakeESFile([binaryPath UTF8String], fileStat);
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
proc.is_platform_binary = false;
|
||||
// Set a 6.5 second deadline for the message. The base SNTEndpointSecurityClient
|
||||
// class leaves a 5 second buffer to auto-respond to messages. A 6 second
|
||||
// deadline means there is a 1.5 second leeway given for the processing block
|
||||
@@ -111,6 +118,10 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth, 6500);
|
||||
esMsg.event.exec.target = &proc;
|
||||
|
||||
if (messageSetupBlock) {
|
||||
messageSetupBlock(&esMsg);
|
||||
}
|
||||
|
||||
// The test must wait for the ES client async message processing to complete.
|
||||
// Otherwise, the `es_message_t` stack variable will go out of scope and will
|
||||
// result in undefined behavior in the async dispatch queue block.
|
||||
@@ -146,6 +157,15 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (BOOL)checkBinaryExecution:(NSString *)binaryName
|
||||
wantResult:(es_auth_result_t)wantResult
|
||||
clientMode:(NSInteger)clientMode {
|
||||
return [self checkBinaryExecution:binaryName
|
||||
wantResult:wantResult
|
||||
clientMode:clientMode
|
||||
messageSetup:nil];
|
||||
}
|
||||
|
||||
/**
|
||||
* testRules ensures that we get the expected outcome when the mocks "execute"
|
||||
* our test binaries.
|
||||
@@ -211,6 +231,66 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
clientMode:SNTClientModeMonitor];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDBlockRuleAndCertAllowedRuleIsBlockedInMonitorMode {
|
||||
[self checkBinaryExecution:@"cert_hash_allowed_signingid_blocked"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kBlockedSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDNoRuleMatchAndCertAllowedRuleIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"cert_hash_allowed_signingid_not_matched"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDBlockRuleMatchAndCertAllowedRuleIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"binary_hash_allowed_signingid_blocked"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kBlockedSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDNoRuleMatchIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"noop"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kNoRuleMatchTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDNoRuleMatchIsBlockedInLockdownMode {
|
||||
[self checkBinaryExecution:@"noop"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeLockdown
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kNoRuleMatchTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithAllowedSigningIDRuleIsAllowedInLockdownMode {
|
||||
[self checkBinaryExecution:@"noop"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeLockdown
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kNoRuleMatchTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kAllowedSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInLockdownMode {
|
||||
[self checkBinaryExecution:@"banned_teamid_allowed_binary"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
|
||||
48
Source/santad/TTYWriter.h
Normal file
48
Source/santad/TTYWriter.h
Normal file
@@ -0,0 +1,48 @@
|
||||
/// 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.
|
||||
|
||||
#ifndef SANTA__SANTAD__TTYWRITER_H
|
||||
#define SANTA__SANTAD__TTYWRITER_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace santa::santad {
|
||||
|
||||
// Small helper class to synchronize writing to TTYs
|
||||
class TTYWriter {
|
||||
public:
|
||||
static std::unique_ptr<TTYWriter> Create();
|
||||
|
||||
TTYWriter(dispatch_queue_t q);
|
||||
|
||||
// Moves can be safe, but not currently needed/implemented
|
||||
TTYWriter(TTYWriter &&other) = delete;
|
||||
TTYWriter &operator=(TTYWriter &&rhs) = delete;
|
||||
|
||||
// No copies
|
||||
TTYWriter(const TTYWriter &other) = delete;
|
||||
TTYWriter &operator=(const TTYWriter &other) = delete;
|
||||
|
||||
void Write(const char *tty, NSString *msg);
|
||||
|
||||
private:
|
||||
dispatch_queue_t q_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
#endif
|
||||
56
Source/santad/TTYWriter.mm
Normal file
56
Source/santad/TTYWriter.mm
Normal file
@@ -0,0 +1,56 @@
|
||||
/// 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.
|
||||
|
||||
#include "Source/santad/TTYWriter.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <sys/errno.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/common/String.h"
|
||||
|
||||
namespace santa::santad {
|
||||
|
||||
std::unique_ptr<TTYWriter> TTYWriter::Create() {
|
||||
dispatch_queue_t q = dispatch_queue_create_with_target(
|
||||
"com.google.santa.ttywriter", DISPATCH_QUEUE_SERIAL,
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
|
||||
|
||||
if (!q) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_unique<TTYWriter>(q);
|
||||
}
|
||||
|
||||
TTYWriter::TTYWriter(dispatch_queue_t q) : q_(q) {}
|
||||
|
||||
void TTYWriter::Write(const char *tty, NSString *msg) {
|
||||
dispatch_async(q_, ^{
|
||||
@autoreleasepool {
|
||||
int fd = open(tty, O_WRONLY | O_NOCTTY);
|
||||
if (fd == -1) {
|
||||
LOGW(@"Failed to open TTY for writing: %s", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string_view str = santa::common::NSStringToUTF8StringView(msg);
|
||||
write(fd, str.data(), str.length());
|
||||
|
||||
close(fd);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace santa::santad
|
||||
BIN
Source/santad/testdata/binaryrules/binary_hash_allowed_signingid_blocked
vendored
Executable file
BIN
Source/santad/testdata/binaryrules/binary_hash_allowed_signingid_blocked
vendored
Executable file
Binary file not shown.
BIN
Source/santad/testdata/binaryrules/cert_hash_allowed_signingid_blocked
vendored
Executable file
BIN
Source/santad/testdata/binaryrules/cert_hash_allowed_signingid_blocked
vendored
Executable file
Binary file not shown.
BIN
Source/santad/testdata/binaryrules/cert_hash_allowed_signingid_not_matched
vendored
Executable file
BIN
Source/santad/testdata/binaryrules/cert_hash_allowed_signingid_not_matched
vendored
Executable file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user