Compare commits

...

47 Commits

Author SHA1 Message Date
Matt W
5a383ebd9a Only eval TID and SID rules when the binary signature is valid (#1191)
* Only eval TID and SID rules when the binary signature is valid

* Simplify setting sid on cached decision
2023-09-28 10:11:01 -04:00
Pete Markowsky
913af692e8 Fix missing Santa block gif. (#1193) 2023-09-27 14:53:45 -04:00
p-harrison
4d6140d047 Update sync-protocol.md (#1187)
Fields like pid, ppid, execution_time, current_sessions etc. are not supplied in Event uploads when the decision is BUNDLE_BINARY (ie. Events generated by the bundle scanning service, rather than actual executions) so I have marked these as not required in the API definition.
Few other small formatting tidy-ups while I was there.
2023-09-19 12:20:42 -04:00
Matt W
2edd2ddfd2 Remove superfluous import (#1188) 2023-09-18 23:01:22 -04:00
Matt W
1515929752 Add ability to specify custom event URLs and button text for FAA dialog (#1186)
* Allow per-policy and per-rule FAA URL and button text

* Add format string support to the custom URL. Added SNTBlockMessageTest.

* Add event URL to TTY message.

* Allow rule specific policy to "clear" global to remove buttons for the rule

* Remove extra beta label for FAA
2023-09-18 22:33:19 -04:00
Pete Markowsky
fc2c7ffb71 Used ratchet to pin GitHub actions to specific hashes. (#1184)
Pin GitHub actions to a specific version.
2023-09-18 15:30:10 -04:00
Kent Ma
98ee36850a Use 'set -xo pipefail' instead (#1185) 2023-09-14 15:37:06 -04:00
Matt W
6f4a48866c Internal build fixes (#1183)
* Address internal build issues

* lint
2023-09-13 22:17:41 -04:00
Matt W
51ca19b238 Fix layering issue for imported module (#1182) 2023-09-13 20:59:07 -04:00
Pete Markowsky
b8d7ed0c07 Add basic support for importing and exporting rules to/from JSON (#1170)
* Add basic support for importing and exporting rules to/from JSON.
2023-09-13 17:46:49 -04:00
Matt W
ff6bf0701d Add ability to override File Access actions via config and sync settings (#1175)
* Support new config (and sync config) option to override file access action.

* Adopt override action config in file access client

* Add sync service and file access client tests

* Require override action to be specific values. Add new sync setting to docs.
2023-09-13 15:47:49 -04:00
Matt W
3be45fd6c0 UI For Blocked File Access (#1174)
* WIP: UI: open cert modal, hookup silence checkbox. Add cert helper funcs.

* Popup dialog on file access violation. Support config-based and custom messages.

* Send message to TTY on file access rule violation

* TTYWriter Write now takes an es_process_t. Fix async data lifespan issue.

* Dedupe TTY message printing per process per rule

* Some minor swift beautification

* Remove main app from dock when showing file access dialog

* Update header docs

* Remove define guards for ObjC header file

* Update Source/common/CertificateHelpers.h

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>

* Fix comment typo

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>

* Use #import for ObjC headers

* Use #import for ObjC header

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>

* lint

* Comment use of escape sequences

---------

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
2023-09-13 15:45:56 -04:00
Matt W
d2e5aec635 Update Protobuf and Abseil versions (#1179) 2023-09-12 11:00:14 -04:00
Pete Markowsky
be1169ffcb Make Transitive Allowlisting Work with Signing ID rules (#1177)
* Make transitive allowlisting work with Signing ID rules

* Update rules.md to include SIGNINGID rules for transitive allowlisting.
2023-09-11 14:28:23 -04:00
Matt W
181c3ae573 Bump bazel and build_bazel_rules_apple versions (#1178)
* Bump bazel and build_bazel_rules_apple versions

* Minor change in Source dir to trigger github build actions

* Declare some archives higher up due to deps changes
2023-09-11 13:41:38 -04:00
Pete Markowsky
5f0755efbf Add Tests for #1165 Behavior. (#1173) 2023-09-04 19:48:44 -04:00
p-harrison
f0165089a4 Update rules.md with more detail on Transitive/Compiler rules (#1172)
Updated the description of Transitive/Compiler rules to clarify that only rules of type BINARY are allowed.
2023-09-01 10:21:19 -04:00
kyoshisuki
5c98ef6897 Update troubleshooting.md (#1169) 2023-08-30 09:01:16 -04:00
p-harrison
e2f8ca9569 Remove logupload stage from syncing-overview.md (#1168)
The logupload stage was referred to in this document but was removed in #331.

FYI this document also refers to santactl performing syncs, which I believe is now handled by santasyncservice, but I am not familiar enough with it to document sorry.
2023-08-29 12:04:33 -04:00
Matt W
2029e239ca Fix issue where client mode was almost always logged as "Unknown" (#1165) 2023-08-28 09:50:21 -04:00
p-harrison
cae3578b62 Document SyncExtraHeaders in configuration.md (#1166)
Document the SyncExtraHeaders configuration option added in #1144
2023-08-28 09:30:12 -04:00
Pete Markowsky
16a8c651d5 Restore file_bundle_hash & file_bundle_binary_count (#1164) 2023-08-25 11:09:02 -04:00
Matt W
4fdc1e5e41 Use default event detail button text when a custom URL is set (#1161) 2023-08-23 15:22:24 +00:00
Matt W
1cdd04f9eb Additional metrics for File Access Authorizer client (#1160)
* WIP additional file access authorizer metrics

* Update flush metrics test. Refactor friend MetricsPeer class.
2023-08-23 15:20:13 +00:00
Matt W
4d0af8838f Fix new buildifier issues (#1162) 2023-08-23 11:18:05 -04:00
p-harrison
0400e29264 Correction to sync-protocol.md (#1159)
Removes  file_bundle_binary_count and file_bunde_hash from the Rule definition and examples

These were accidentally added to the Rule definition and examples, rather than to the Event section in #1130.

Co-authored-by: Pete Markowsky <pmarkowsky@users.noreply.github.com>
2023-08-21 12:19:51 -04:00
p-harrison
2c6da7158d Add SigningID/TeamID to Event definition in sync-protocol.md (#1158)
Added SigningID/TeamID to Event definition

Added SigningID and TeamID to the definition of Events in the EventUpload stage

Documented SigningID and TeamID in the definition of Events in the EventUpload stage
2023-08-21 09:54:56 -04:00
Russell Hancox
b0ab761568 sync: Send rules_received and rules_processed fields in postflight request (#1156) 2023-08-19 00:45:49 +02:00
Matt W
b02336613a Remove references to old EnableSystemExtension config key (#1155) 2023-08-18 11:47:14 -04:00
Matt W
bd86145679 Add mount from information to disk appear events (#1153) 2023-08-17 08:00:01 -04:00
Matt W
6dfd5ba084 Fix issue where re config types couldn't be overridden (#1151) 2023-08-14 23:40:48 +02:00
Pete Markowsky
72e292d80e Add support for was_mmaped_writeable to file write monitoring when using macOS 13+ (#1148)
Add support for was_mmaped_writeable to file write monitoring when using macOS 13

In macOS 13 close events now have a new field was_mapped_writable that lets us
track if the file was mmaped writable.  Often developer tools use mmap to
avoid large numbers of write syscalls (e.g. the go toolchain) and this improves
transitive allow listing with those tools.
2023-08-14 15:25:48 -04:00
p-harrison
6588c2342b Added TransitiveWhitelisting explanation to rules.md (#1150)
* Added TransitiveWhitelisting explanation to rules.md

Added a section to explain TransitiveWhitelisting and Transitive/Compiler rules

* Update docs/concepts/rules.md

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

* Update docs/concepts/rules.md

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

---------

Co-authored-by: Matt W <436037+mlw@users.noreply.github.com>
2023-08-14 12:04:24 -04:00
Russell Hancox
d82e64aa5f Project: Split integration VM license into its own LICENSE file (#1147) 2023-08-08 13:29:06 -04:00
Ivan Tadeu Ferreira Antunes Filho
a9c1c730be Project: Cast some enums to int (#1146)
Allows the string displaying the enum to format it using %d.

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

* Clear cache on policy change

* Prevent unbounded cache growth

* Move cache impl to its own class

* Add some additional tests

* Cleanup

* Comment cleanup

* Switch to absl containers

* Use default absl::Hash instead of custom hasher

* Removing another reference to PairHash

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

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

* Minor comment fixes, more test cases

* Handle SigningIDs using the delimiter character

* lint

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

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

Couple of formatting changes, added SIGNINGID as a rule type

* Update docs/development/sync-protocol.md

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

---------

Co-authored-by: Matt W <436037+mlw@users.noreply.github.com>
2023-07-25 14:31:13 -04:00
110 changed files with 3235 additions and 803 deletions

View File

@@ -9,6 +9,7 @@ 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
build --host_cxxopt=-std=c++17
build --copt=-DSANTA_OPEN_SOURCE=1
build --cxxopt=-DSANTA_OPEN_SOURCE=1

View File

@@ -1 +1 @@
5.3.0
6.3.2

View File

@@ -1,5 +1,4 @@
name: CI
on:
push:
branches:
@@ -11,15 +10,13 @@ on:
- main
paths:
- 'Source/**'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Run linters
run: ./Testing/lint.sh
build_userspace:
strategy:
fail-fast: false
@@ -27,10 +24,9 @@ jobs:
os: [macos-11, macos-12, macos-13]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Build Userspace
run: bazel build --apple_generate_dsym -c opt :release --define=SANTA_BUILD_TYPE=adhoc
unit_tests:
strategy:
fail-fast: false
@@ -38,18 +34,17 @@ jobs:
os: [macos-11, macos-12, macos-13]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Run All Tests
run: bazel test :unit_tests --define=SANTA_BUILD_TYPE=adhoc --test_output=errors
test_coverage:
runs-on: macos-11
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Generate test coverage
run: sh ./generate_cov.sh
- name: Coveralls
uses: coverallsapp/github-action@master
uses: coverallsapp/github-action@09b709cf6a16e30b0808ba050c7a6e8a5ef13f8d # ratchet:coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./bazel-out/_coverage/_coverage_report.dat

View File

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

View File

@@ -140,7 +140,7 @@ A tool like Santa doesn't really lend itself to screenshots, so here's a video
instead.
<p align="center"> <img src="https://thumbs.gfycat.com/MadFatalAmphiuma-small.gif" alt="Santa Block Video" /> </p>
<p align="center"> <img src="./docs/images/santa-block.gif" alt="Santa Block Video" /> </p>
# Contributing
Patches to this project are very much welcome. Please see the

View File

@@ -1,5 +1,5 @@
load("//:helper.bzl", "santa_unit_test")
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
load("//:helper.bzl", "santa_unit_test")
package(
default_visibility = ["//:santa_package_group"],
@@ -84,12 +84,22 @@ objc_library(
],
)
objc_library(
name = "CertificateHelpers",
srcs = ["CertificateHelpers.m"],
hdrs = ["CertificateHelpers.h"],
deps = [
"@MOLCertificate",
],
)
objc_library(
name = "SNTBlockMessage",
srcs = ["SNTBlockMessage.m"],
hdrs = ["SNTBlockMessage.h"],
deps = [
":SNTConfigurator",
":SNTFileAccessEvent",
":SNTLogging",
":SNTStoredEvent",
":SNTSystemInfo",
@@ -103,7 +113,7 @@ objc_library(
defines = ["SANTAGUI"],
deps = [
":SNTConfigurator",
":SNTDeviceEvent",
":SNTFileAccessEvent",
":SNTLogging",
":SNTStoredEvent",
":SNTSystemInfo",
@@ -142,6 +152,7 @@ objc_library(
"Foundation",
],
deps = [
":CertificateHelpers",
"@MOLCertificate",
],
)
@@ -238,7 +249,11 @@ objc_library(
santa_unit_test(
name = "SNTRuleTest",
srcs = ["SNTRuleTest.m"],
deps = [":SNTRule"],
deps = [
":SNTCommonEnums",
":SNTRule",
":SNTSyncConstants",
],
)
objc_library(
@@ -393,10 +408,24 @@ santa_unit_test(
],
)
santa_unit_test(
name = "SNTBlockMessageTest",
srcs = ["SNTBlockMessageTest.m"],
deps = [
":SNTBlockMessage",
":SNTConfigurator",
":SNTFileAccessEvent",
":SNTStoredEvent",
":SNTSystemInfo",
"@OCMock",
],
)
test_suite(
name = "unit_tests",
tests = [
":PrefixTreeTest",
":SNTBlockMessageTest",
":SNTCachedDecisionTest",
":SNTFileInfoTest",
":SNTKVOManagerTest",

View 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.
#import <Foundation/Foundation.h>
#import <MOLCertificate/MOLCertificate.h>
#include <sys/cdefs.h>
__BEGIN_DECLS
/**
Return a string representing publisher info from the provided certs
@param certs A certificate chain
@param teamID A team ID to be displayed for apps from the App Store
@return A string that tries to be more helpful to users by extracting
appropriate information from the certificate chain.
*/
NSString *Publisher(NSArray<MOLCertificate *> *certs, NSString *teamID);
/**
Return an array of the underlying SecCertificateRef's for the given array
of MOLCertificates.
@param certs An array of MOLCertificates
@return An array of SecCertificateRefs. WARNING: If the refs need to be used
for a long time be careful to properly CFRetain/CFRelease the returned items.
*/
NSArray<id> *CertificateChain(NSArray<MOLCertificate *> *certs);
__END_DECLS

View File

@@ -0,0 +1,42 @@
/// 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/CertificateHelpers.h"
#include <Security/SecCertificate.h>
NSString *Publisher(NSArray<MOLCertificate *> *certs, NSString *teamID) {
MOLCertificate *leafCert = [certs firstObject];
if ([leafCert.commonName isEqualToString:@"Apple Mac OS Application Signing"]) {
return [NSString stringWithFormat:@"App Store (Team ID: %@)", teamID];
} else if (leafCert.commonName && leafCert.orgName) {
return [NSString stringWithFormat:@"%@ - %@", leafCert.orgName, leafCert.commonName];
} else if (leafCert.commonName) {
return leafCert.commonName;
} else if (leafCert.orgName) {
return leafCert.orgName;
} else {
return nil;
}
}
NSArray<id> *CertificateChain(NSArray<MOLCertificate *> *certs) {
NSMutableArray *certArray = [NSMutableArray arrayWithCapacity:[certs count]];
for (MOLCertificate *cert in certs) {
[certArray addObject:(id)cert.certRef];
}
return certArray;
}

View File

@@ -18,6 +18,9 @@
#import <Foundation/Foundation.h>
#endif
#import "Source/common/SNTFileAccessEvent.h"
#import "Source/common/SNTStoredEvent.h"
@class SNTStoredEvent;
@interface SNTBlockMessage : NSObject
@@ -38,11 +41,15 @@
+ (NSAttributedString *)attributedBlockMessageForEvent:(SNTStoredEvent *)event
customMessage:(NSString *)customMessage;
+ (NSAttributedString *)attributedBlockMessageForFileAccessEvent:(SNTFileAccessEvent *)event
customMessage:(NSString *)customMessage;
///
/// Return a URL generated from the EventDetailURL configuration key
/// after replacing templates in the URL with values from the event.
///
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event;
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url;
+ (NSURL *)eventDetailURLForFileAccessEvent:(SNTFileAccessEvent *)event customURL:(NSString *)url;
///
/// Strip HTML from a string, replacing <br /> with newline.

View File

@@ -15,6 +15,7 @@
#import "Source/common/SNTBlockMessage.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTFileAccessEvent.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/SNTSystemInfo.h"
@@ -82,6 +83,18 @@
return [SNTBlockMessage formatMessage:message];
}
+ (NSAttributedString *)attributedBlockMessageForFileAccessEvent:(SNTFileAccessEvent *)event
customMessage:(NSString *)customMessage {
NSString *message = customMessage;
if (!message.length) {
message = [[SNTConfigurator configurator] fileAccessBlockMessage];
if (!message.length) {
message = @"Access to a file has been denied.";
}
}
return [SNTBlockMessage formatMessage:message];
}
+ (NSString *)stringFromHTML:(NSString *)html {
NSError *error;
NSXMLDocument *xml = [[NSXMLDocument alloc] initWithXMLString:html options:0 error:&error];
@@ -109,46 +122,127 @@
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event {
+ (NSString *)replaceFormatString:(NSString *)str
withDict:(NSDictionary<NSString *, NSString * (^)()> *)replacements {
__block NSString *formatStr = str;
[replacements
enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString * (^computeValue)(), BOOL *stop) {
NSString *value = computeValue();
if (value) {
formatStr = [formatStr stringByReplacingOccurrencesOfString:key withString:value];
}
}];
return formatStr;
}
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
// The following "format strings" will be replaced in the URL, if they are present:
//
// %file_identifier% - The SHA-256 of the binary being executed.
// %bundle_or_file_identifier% - The hash of the bundle containing this file or the file itself,
// if no bundle hash is present.
// %username% - The executing user's name.
// %machine_id% - The configured machine ID for this host.
// %hostname% - The machine's FQDN.
// %uuid% - The machine's UUID.
// %serial% - The machine's serial number.
//
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url {
SNTConfigurator *config = [SNTConfigurator configurator];
NSString *hostname = [SNTSystemInfo longHostname];
NSString *uuid = [SNTSystemInfo hardwareUUID];
NSString *serial = [SNTSystemInfo serialNumber];
NSString *formatStr = config.eventDetailURL;
if (!formatStr.length) return nil;
if (event.fileSHA256) {
// This key is deprecated, use %file_identifier% or %bundle_or_file_identifier%
formatStr =
[formatStr stringByReplacingOccurrencesOfString:@"%file_sha%"
withString:event.fileBundleHash ?: event.fileSHA256];
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%file_identifier%"
withString:event.fileSHA256];
formatStr =
[formatStr stringByReplacingOccurrencesOfString:@"%bundle_or_file_identifier%"
withString:event.fileBundleHash ?: event.fileSHA256];
}
if (event.executingUser) {
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%username%"
withString:event.executingUser];
}
if (config.machineID) {
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%machine_id%"
withString:config.machineID];
}
if (hostname.length) {
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%hostname%" withString:hostname];
}
if (uuid.length) {
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%uuid%" withString:uuid];
}
if (serial.length) {
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%serial%" withString:serial];
NSString *formatStr = url;
if (!formatStr.length) {
formatStr = config.eventDetailURL;
if (!formatStr.length) {
return nil;
}
}
return [NSURL URLWithString:formatStr];
if ([formatStr isEqualToString:@"null"]) {
return nil;
}
// Disabling clang-format. See comment in `eventDetailURLForFileAccessEvent:customURL:`
// clang-format off
NSDictionary<NSString *, NSString * (^)()> *kvReplacements =
[NSDictionary dictionaryWithObjectsAndKeys:
// This key is deprecated, use %file_identifier% or %bundle_or_file_identifier%
^{ return event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil; },
@"%file_sha%",
^{ return event.fileSHA256; }, @"%file_identifier%",
^{ return event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil; },
@"%bundle_or_file_identifier%",
^{ return event.executingUser; }, @"%username%",
^{ return config.machineID; }, @"%machine_id%",
^{ return [SNTSystemInfo longHostname]; }, @"%hostname%",
^{ return [SNTSystemInfo hardwareUUID]; }, @"%uuid%",
^{ return [SNTSystemInfo serialNumber]; }, @"%serial%",
nil];
// clang-format on
formatStr = [SNTBlockMessage replaceFormatString:formatStr withDict:kvReplacements];
NSURL *u = [NSURL URLWithString:formatStr];
if (!u) {
LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
}
return u;
}
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
// The following "format strings" will be replaced in the URL, if they are present:
//
// %rule_version% - The version of the rule that was violated.
// %rule_name% - The name of the rule that was violated.
// %file_identifier% - The SHA-256 of the binary being executed.
// %accessed_path% - The path accessed by the binary.
// %username% - The executing user's name.
// %machine_id% - The configured machine ID for this host.
// %hostname% - The machine's FQDN.
// %uuid% - The machine's UUID.
// %serial% - The machine's serial number.
//
+ (NSURL *)eventDetailURLForFileAccessEvent:(SNTFileAccessEvent *)event customURL:(NSString *)url {
if (!url.length || [url isEqualToString:@"null"]) {
return nil;
}
SNTConfigurator *config = [SNTConfigurator configurator];
// Clang format goes wild here. If you use the container literal syntax `@{}` with a block value
// type, it seems to break the clang format on/off functionality and breaks formatting for the
// remainder of the file.
// Using `dictionaryWithObjectsAndKeys` and disabling clang format as a workaround.
// clang-format off
NSDictionary<NSString *, NSString * (^)()> *kvReplacements =
[NSDictionary dictionaryWithObjectsAndKeys:
^{ return event.ruleVersion; }, @"%rule_version%",
^{ return event.ruleName; }, @"%rule_name%",
^{ return event.fileSHA256; }, @"%file_identifier%",
^{ return event.accessedPath; }, @"%accessed_path%",
^{ return event.executingUser; }, @"%username%",
^{ return config.machineID; }, @"%machine_id%",
^{ return [SNTSystemInfo longHostname]; }, @"%hostname%",
^{ return [SNTSystemInfo hardwareUUID]; }, @"%uuid%",
^{ return [SNTSystemInfo serialNumber]; }, @"%serial%",
nil];
// clang-format on
NSString *formatStr = [SNTBlockMessage replaceFormatString:url withDict:kvReplacements];
NSURL *u = [NSURL URLWithString:formatStr];
if (!u) {
LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
}
return u;
}
@end

View File

@@ -0,0 +1,95 @@
/// 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 <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#import "Source/common/SNTBlockMessage.h"
#import "Source/common/SNTConfigurator.h"
#include "Source/common/SNTFileAccessEvent.h"
#include "Source/common/SNTStoredEvent.h"
#import "Source/common/SNTSystemInfo.h"
@interface SNTBlockMessageTest : XCTestCase
@property id mockConfigurator;
@property id mockSystemInfo;
@end
@implementation SNTBlockMessageTest
- (void)setUp {
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
OCMStub([self.mockConfigurator machineID]).andReturn(@"my_mid");
self.mockSystemInfo = OCMClassMock([SNTSystemInfo class]);
OCMStub([self.mockSystemInfo longHostname]).andReturn(@"my_hn");
OCMStub([self.mockSystemInfo hardwareUUID]).andReturn(@"my_u");
OCMStub([self.mockSystemInfo serialNumber]).andReturn(@"my_s");
}
- (void)testEventDetailURLForEvent {
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
se.fileSHA256 = @"my_fi";
se.executingUser = @"my_un";
NSString *url = @"http://"
@"localhost?fs=%file_sha%&fi=%file_identifier%&bfi=%bundle_or_file_identifier%&"
@"un=%username%&mid=%machine_id%&hn=%hostname%&u=%uuid%&s=%serial%";
NSString *wantUrl =
@"http://"
@"localhost?fs=my_fi&fi=my_fi&bfi=my_fi&bfi=my_fi&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
// Set fileBundleHash and test again for newly expected values
se.fileBundleHash = @"my_fbh";
wantUrl = @"http://"
@"localhost?fs=my_fbh&fi=my_fi&bfi=my_fbh&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
XCTAssertEqualObjects(gotUrl.absoluteString, wantUrl);
XCTAssertNil([SNTBlockMessage eventDetailURLForEvent:se customURL:nil]);
XCTAssertNil([SNTBlockMessage eventDetailURLForEvent:se customURL:@"null"]);
}
- (void)testEventDetailURLForFileAccessEvent {
SNTFileAccessEvent *fae = [[SNTFileAccessEvent alloc] init];
fae.ruleVersion = @"my_rv";
fae.ruleName = @"my_rn";
fae.fileSHA256 = @"my_fi";
fae.accessedPath = @"my_ap";
fae.executingUser = @"my_un";
NSString *url = @"http://"
@"localhost?rv=%rule_version%&rn=%rule_name%&fi=%file_identifier%&ap=%accessed_"
@"path%&un=%username%&mid=%machine_id%&hn=%hostname%&u=%uuid%&s=%serial%";
NSString *wantUrl =
@"http://"
@"localhost?rv=my_rv&rn=my_rn&fi=my_fi&ap=my_ap&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:url];
XCTAssertEqualObjects(gotUrl.absoluteString, wantUrl);
XCTAssertNil([SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:nil]);
XCTAssertNil([SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:@"null"]);
}
@end

View File

@@ -42,6 +42,7 @@
@property NSString *quarantineURL;
@property NSString *customMsg;
@property NSString *customURL;
@property BOOL silentBlock;
@end

View File

@@ -152,6 +152,12 @@ typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
SNTMetricFormatTypeMonarchJSON,
};
typedef NS_ENUM(NSInteger, SNTOverrideFileAccessAction) {
SNTOverrideFileAccessActionNone,
SNTOverrideFileAccessActionAuditOnly,
SNTOverrideFileAccessActionDiable,
};
#ifdef __cplusplus
enum class FileAccessPolicyDecision {
kNoPolicy,

View File

@@ -262,6 +262,16 @@
///
@property(readonly, nonatomic) NSString *fileAccessPolicyPlist;
///
/// This is the message shown to the user when access to a file is blocked
/// by a binary due to some rule in the current File Access policy if that rule
/// doesn't provide a custom message. If this is not configured, a reasonable
/// default is provided.
///
/// @note: This property is KVO compliant.
///
@property(readonly, nonatomic) NSString *fileAccessBlockMessage;
///
/// If fileAccessPolicyPlist is set, fileAccessPolicyUpdateIntervalSec
/// sets the number of seconds between times that the configuration file is
@@ -390,6 +400,27 @@
///
@property(readonly, nonatomic) NSDictionary *syncProxyConfig;
///
/// Extra headers to include in all requests made during syncing.
/// Keys and values must all be strings, any other type will be silently ignored.
/// Some headers cannot be set through this key, including:
///
/// * Content-Encoding
/// * Content-Length
/// * Content-Type
/// * Connection
/// * Host
/// * Proxy-Authenticate
/// * Proxy-Authorization
/// * WWW-Authenticate
///
/// The header "Authorization" is also documented by Apple to be one that will
/// be ignored but this is not really the case, at least at present. If you
/// are able to use a different header for this that would be safest but if not
/// using Authorization /should/ be fine.
///
@property(readonly, nonatomic) NSDictionary *syncExtraHeaders;
///
/// The machine owner.
///
@@ -423,6 +454,25 @@
///
@property(nonatomic) NSArray<NSString *> *remountUSBMode;
///
/// If set, will override the action taken when a file access rule violation
/// occurs. This setting will apply across all rules in the file access policy.
///
/// Possible values are
/// * "AuditOnly": When a rule is violated, it will be logged, but the access
/// will not be blocked
/// * "Disable": No access will be logged or blocked.
///
/// If not set, no override will take place and the file acces spolicy will
/// apply as configured.
///
@property(readonly, nonatomic) SNTOverrideFileAccessAction overrideFileAccessAction;
///
/// Set the action that will override file access policy config action
///
- (void)setSyncServerOverrideFileAccessAction:(NSString *)action;
///
/// If set, this over-rides the default machine ID used for syncing.
///

View File

@@ -13,7 +13,6 @@
/// limitations under the License.
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTCommonEnums.h"
#include <sys/stat.h>
@@ -57,6 +56,7 @@ static NSString *const kMobileConfigDomain = @"com.google.santa";
static NSString *const kStaticRules = @"StaticRules";
static NSString *const kSyncBaseURLKey = @"SyncBaseURL";
static NSString *const kSyncProxyConfigKey = @"SyncProxyConfiguration";
static NSString *const kSyncExtraHeadersKey = @"SyncExtraHeaders";
static NSString *const kSyncEnableCleanSyncEventUpload = @"SyncEnableCleanSyncEventUpload";
static NSString *const kClientAuthCertificateFileKey = @"ClientAuthCertificateFile";
static NSString *const kClientAuthCertificatePasswordKey = @"ClientAuthCertificatePassword";
@@ -101,6 +101,7 @@ static NSString *const kSpoolDirectoryEventMaxFlushTimeSec = @"SpoolDirectoryEve
static NSString *const kFileAccessPolicy = @"FileAccessPolicy";
static NSString *const kFileAccessPolicyPlist = @"FileAccessPolicyPlist";
static NSString *const kFileAccessBlockMessage = @"FileAccessBlockMessage";
static NSString *const kFileAccessPolicyUpdateIntervalSec = @"FileAccessPolicyUpdateIntervalSec";
static NSString *const kEnableMachineIDDecoration = @"EnableMachineIDDecoration";
@@ -128,6 +129,7 @@ static NSString *const kBlockedPathRegexKey = @"BlockedPathRegex";
static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
static NSString *const kOverrideFileAccessActionKey = @"OverrideFileAccessAction";
static NSString *const kMetricFormat = @"MetricFormat";
static NSString *const kMetricURL = @"MetricURL";
@@ -164,6 +166,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kRuleSyncLastSuccess : date,
kSyncCleanRequired : number,
kEnableAllEventUploadKey : number,
kOverrideFileAccessActionKey : string,
};
_forcedConfigKeyTypes = @{
kClientModeKey : number,
@@ -196,6 +199,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kSyncBaseURLKey : string,
kSyncEnableCleanSyncEventUpload : number,
kSyncProxyConfigKey : dictionary,
kSyncExtraHeadersKey : dictionary,
kClientAuthCertificateFileKey : string,
kClientAuthCertificatePasswordKey : string,
kClientAuthCertificateCNKey : string,
@@ -217,6 +221,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kSpoolDirectoryEventMaxFlushTimeSec : number,
kFileAccessPolicy : dictionary,
kFileAccessPolicyPlist : string,
kFileAccessBlockMessage : string,
kFileAccessPolicyUpdateIntervalSec : number,
kEnableMachineIDDecoration : number,
kEnableForkAndExitLogging : number,
@@ -232,6 +237,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kMetricExtraLabels : dictionary,
kEnableAllEventUploadKey : number,
kDisableUnknownEventUploadKey : number,
kOverrideFileAccessActionKey : string,
};
_defaults = [NSUserDefaults standardUserDefaults];
[_defaults addSuiteNamed:@"com.google.santa"];
@@ -315,6 +321,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingSyncExtraHeaders {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEnableCleanSyncEventUpload {
return [self configStateSet];
}
@@ -435,6 +445,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingFileAccessBlockMessage {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyUpdateIntervalSec {
return [self configStateSet];
}
@@ -507,6 +521,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingOverrideFileAccessActionKey {
return [self syncAndConfigStateSet];
}
#pragma mark Public Interface
- (SNTClientMode)clientMode {
@@ -632,6 +650,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return self.configState[kSyncProxyConfigKey];
}
- (NSDictionary *)syncExtraHeaders {
return self.configState[kSyncExtraHeadersKey];
}
- (BOOL)enablePageZeroProtection {
NSNumber *number = self.configState[kEnablePageZeroProtectionKey];
return number ? [number boolValue] : YES;
@@ -851,6 +873,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
}
}
- (NSString *)fileAccessBlockMessage {
return self.configState[kFileAccessBlockMessage];
}
- (uint32_t)fileAccessPolicyUpdateIntervalSec {
return self.configState[kFileAccessPolicyUpdateIntervalSec]
? [self.configState[kFileAccessPolicyUpdateIntervalSec] unsignedIntValue]
@@ -931,6 +957,33 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self.configState[kBlockUSBMountKey] boolValue];
}
- (void)setSyncServerOverrideFileAccessAction:(NSString *)action {
NSString *a = [action lowercaseString];
if ([a isEqualToString:@"auditonly"] || [a isEqualToString:@"disable"] ||
[a isEqualToString:@"none"] || [a isEqualToString:@""]) {
[self updateSyncStateForKey:kOverrideFileAccessActionKey value:action];
}
}
- (SNTOverrideFileAccessAction)overrideFileAccessAction {
NSString *action = [self.syncState[kOverrideFileAccessActionKey] lowercaseString];
if (!action) {
action = [self.configState[kOverrideFileAccessActionKey] lowercaseString];
if (!action) {
return SNTOverrideFileAccessActionNone;
}
}
if ([action isEqualToString:@"auditonly"]) {
return SNTOverrideFileAccessActionAuditOnly;
} else if ([action isEqualToString:@"disable"]) {
return SNTOverrideFileAccessActionDiable;
} else {
return SNTOverrideFileAccessActionNone;
}
}
///
/// Returns YES if all of the necessary options are set to export metrics, NO
/// otherwise.
@@ -1063,7 +1116,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
NSDictionary *overrides = [NSDictionary dictionaryWithContentsOfFile:kConfigOverrideFilePath];
for (NSString *key in overrides) {
id obj = overrides[key];
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]]) continue;
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]] ||
([self.forcedConfigKeyTypes[key] isKindOfClass:[NSRegularExpression class]] &&
![obj isKindOfClass:[NSString class]])) {
continue;
}
forcedConfig[key] = obj;
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;

View File

@@ -77,7 +77,23 @@
///
@property NSString *parentName;
// TODO(mlw): Store signing chain info
// @property NSArray<MOLCertificate*> *signingChain;
///
/// If the executed file was signed, this is an NSArray of MOLCertificate's
/// representing the signing chain.
///
@property NSArray<MOLCertificate *> *signingChain;
///
/// A string representing the publisher based on the signingChain
///
@property(readonly) NSString *publisherInfo;
///
/// Return an array of the underlying SecCertificateRef's of the signingChain
///
/// WARNING: If the refs need to be used for a long time be careful to properly
/// CFRetain/CFRelease the returned items.
///
@property(readonly) NSArray *signingChainCertRefs;
@end

View File

@@ -14,6 +14,8 @@
#import "Source/common/SNTFileAccessEvent.h"
#import "Source/common/CertificateHelpers.h"
@implementation SNTFileAccessEvent
#define ENCODE(o) \
@@ -28,6 +30,12 @@
_##o = [decoder decodeObjectOfClass:[c class] forKey:@(#o)]; \
} while (0)
#define DECODEARRAY(o, c) \
do { \
_##o = [decoder decodeObjectOfClasses:[NSSet setWithObjects:[NSArray class], [c class], nil] \
forKey:@(#o)]; \
} while (0)
- (instancetype)init {
self = [super init];
if (self) {
@@ -51,6 +59,7 @@
ENCODE(pid);
ENCODE(ppid);
ENCODE(parentName);
ENCODE(signingChain);
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
@@ -67,6 +76,7 @@
DECODE(pid, NSNumber);
DECODE(ppid, NSNumber);
DECODE(parentName, NSString);
DECODEARRAY(signingChain, MOLCertificate);
}
return self;
}
@@ -76,4 +86,12 @@
stringWithFormat:@"SNTFileAccessEvent: Accessed: %@, By: %@", self.accessedPath, self.filePath];
}
- (NSString *)publisherInfo {
return Publisher(self.signingChain, self.teamID);
}
- (NSArray *)signingChainCertRefs {
return CertificateChain(self.signingChain);
}
@end

View File

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

View File

@@ -13,8 +13,15 @@
/// limitations under the License.
#import "Source/common/SNTRule.h"
#include <CommonCrypto/CommonCrypto.h>
#include <os/base.h>
#import "Source/common/SNTSyncConstants.h"
// https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/
static const NSUInteger kExpectedTeamIDLength = 10;
@interface SNTRule ()
@property(readwrite) NSUInteger timestamp;
@end
@@ -28,20 +35,84 @@
timestamp:(NSUInteger)timestamp {
self = [super init];
if (self) {
if (identifier.length == 0) {
return nil;
}
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdef"] invertedSet];
NSCharacterSet *nonUppercaseAlphaNumeric = [[NSCharacterSet
characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"] invertedSet];
switch (type) {
case SNTRuleTypeBinary: OS_FALLTHROUGH;
case SNTRuleTypeCertificate: {
// For binary and certificate rules, force the hash identifier to be lowercase hex.
identifier = [identifier lowercaseString];
identifier = [identifier stringByTrimmingCharactersInSet:nonHex];
if (identifier.length != (CC_SHA256_DIGEST_LENGTH * 2)) {
return nil;
}
break;
}
case SNTRuleTypeTeamID: {
// TeamIDs are always [0-9A-Z], so enforce that the identifier is uppercase
identifier =
[[identifier uppercaseString] stringByTrimmingCharactersInSet:nonUppercaseAlphaNumeric];
if (identifier.length != kExpectedTeamIDLength) {
return nil;
}
break;
}
case SNTRuleTypeSigningID: {
// SigningID rules are a combination of `TeamID:SigningID`. The TeamID should
// be forced to be uppercase, but because very loose rules exist for SigningIDs,
// their case will be kept as-is. However, platform binaries are expected to
// have the hardcoded string "platform" as the team ID and the case will be left
// as is.
NSArray *sidComponents = [identifier componentsSeparatedByString:@":"];
if (!sidComponents || sidComponents.count < 2) {
return nil;
}
// The first component is the TeamID
NSString *teamID = sidComponents[0];
if (![teamID isEqualToString:@"platform"]) {
teamID =
[[teamID uppercaseString] stringByTrimmingCharactersInSet:nonUppercaseAlphaNumeric];
if (teamID.length != kExpectedTeamIDLength) {
return nil;
}
}
// The rest of the components are the Signing ID since ":" a legal character.
// Join all but the last element of the components to rebuild the SigningID.
NSString *signingID = [[sidComponents
subarrayWithRange:NSMakeRange(1, sidComponents.count - 1)] componentsJoinedByString:@":"];
if (signingID.length == 0) {
return nil;
}
identifier = [NSString stringWithFormat:@"%@:%@", teamID, signingID];
break;
}
default: {
break;
}
}
_identifier = identifier;
_state = state;
_type = type;
_customMsg = customMsg;
_timestamp = timestamp;
if (_type == SNTRuleTypeBinary || _type == SNTRuleTypeCertificate) {
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
if ([[_identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length != 64)
return nil;
} else if (_identifier.length == 0) {
return nil;
}
}
return self;
}
@@ -111,7 +182,14 @@
customMsg = nil;
}
return [self initWithIdentifier:identifier state:state type:type customMsg:customMsg];
NSString *customURL = dict[kRuleCustomURL];
if (![customURL isKindOfClass:[NSString class]] || customURL.length == 0) {
customURL = nil;
}
SNTRule *r = [self initWithIdentifier:identifier state:state type:type customMsg:customMsg];
r.customURL = customURL;
return r;
}
#pragma mark NSSecureCoding
@@ -131,6 +209,7 @@
ENCODE(@(self.state), @"state");
ENCODE(@(self.type), @"type");
ENCODE(self.customMsg, @"custommsg");
ENCODE(self.customURL, @"customurl");
ENCODE(@(self.timestamp), @"timestamp");
}
@@ -141,11 +220,49 @@
_state = [DECODE(NSNumber, @"state") intValue];
_type = [DECODE(NSNumber, @"type") intValue];
_customMsg = DECODE(NSString, @"custommsg");
_customURL = DECODE(NSString, @"customurl");
_timestamp = [DECODE(NSNumber, @"timestamp") unsignedIntegerValue];
}
return self;
}
- (NSString *)ruleStateToPolicyString:(SNTRuleState)state {
switch (state) {
case SNTRuleStateAllow: return kRulePolicyAllowlist;
case SNTRuleStateAllowCompiler: return kRulePolicyAllowlistCompiler;
case SNTRuleStateBlock: return kRulePolicyBlocklist;
case SNTRuleStateSilentBlock: return kRulePolicySilentBlocklist;
case SNTRuleStateRemove: return kRulePolicyRemove;
case SNTRuleStateAllowTransitive: return @"AllowTransitive";
// This should never be hit. But is here for completion.
default: return @"Unknown";
}
}
- (NSString *)ruleTypeToString:(SNTRuleType)ruleType {
switch (ruleType) {
case SNTRuleTypeBinary: return kRuleTypeBinary;
case SNTRuleTypeCertificate: return kRuleTypeCertificate;
case SNTRuleTypeTeamID: return kRuleTypeTeamID;
case SNTRuleTypeSigningID: return kRuleTypeSigningID;
// This should never be hit. If we have rule types of Unknown then there's a
// coding error somewhere.
default: return @"Unknown";
}
}
// Returns an NSDictionary representation of the rule. Primarily use for
// exporting rules.
- (NSDictionary *)dictionaryRepresentation {
return @{
kRuleIdentifier : self.identifier,
kRulePolicy : [self ruleStateToPolicyString:self.state],
kRuleType : [self ruleTypeToString:self.type],
kRuleCustomMsg : self.customMsg ?: @"",
kRuleCustomURL : self.customURL ?: @""
};
}
#undef DECODE
#undef ENCODE
#pragma clang diagnostic pop

View File

@@ -13,6 +13,8 @@
/// limitations under the License.
#import <XCTest/XCTest.h>
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTRule.h"
@@ -46,13 +48,25 @@
XCTAssertEqual(sut.type, SNTRuleTypeCertificate);
XCTAssertEqual(sut.state, SNTRuleStateBlock);
// Ensure a Binary and Certificate rules properly convert identifiers to lowercase.
for (NSString *ruleType in @[ @"BINARY", @"CERTIFICATE" ]) {
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"B7C1E3FD640C5F211C89B02C2C6122F78CE322AA5C56EB0BB54BC422A8F8B670",
@"policy" : @"BLOCKLIST",
@"rule_type" : ruleType,
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
}
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"ABCDEFGHIJ",
@"policy" : @"SILENT_BLOCKLIST",
@"rule_type" : @"TEAMID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
XCTAssertEqual(sut.state, SNTRuleStateSilentBlock);
@@ -68,26 +82,114 @@
XCTAssertEqual(sut.state, SNTRuleStateAllowCompiler);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"ABCDEFGHIJ",
@"policy" : @"REMOVE",
@"rule_type" : @"TEAMID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
XCTAssertEqual(sut.state, SNTRuleStateRemove);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"ABCDEFGHIJ",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
@"custom_msg" : @"A custom block message",
@"custom_url" : @"https://example.com",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
XCTAssertEqual(sut.state, SNTRuleStateAllow);
XCTAssertEqualObjects(sut.customMsg, @"A custom block message");
XCTAssertEqualObjects(sut.customURL, @"https://example.com");
// TeamIDs must be 10 chars in length
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"A",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
}];
XCTAssertNil(sut);
// TeamIDs must be only alphanumeric chars
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"ßßßßßßßßßß",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
}];
XCTAssertNil(sut);
// TeamIDs are converted to uppercase
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"abcdefghij",
@"policy" : @"REMOVE",
@"rule_type" : @"TEAMID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
// SigningID tests
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"ABCDEFGHIJ:com.example",
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ:com.example");
XCTAssertEqual(sut.type, SNTRuleTypeSigningID);
XCTAssertEqual(sut.state, SNTRuleStateRemove);
// Invalid SingingID tests:
for (NSString *ident in @[
@":com.example", // missing team ID
@"ABCDEFGHIJ:", // missing signing ID
@"ABC:com.example", // Invalid team id
@":", // missing team and signing IDs
@"", // empty string
]) {
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : ident,
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNil(sut);
}
// Signing ID with lower team ID has case fixed up
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"abcdefghij:com.example",
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ:com.example");
// Signing ID with lower platform team ID is left alone
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"platform:com.example",
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"platform:com.example");
// Signing ID can contain the TID:SID delimiter character (":")
for (NSString *ident in @[
@"ABCDEFGHIJ:com:",
@"ABCDEFGHIJ:com:example",
@"ABCDEFGHIJ::",
@"ABCDEFGHIJ:com:example:with:more:components:",
]) {
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : ident,
@"policy" : @"ALLOWLIST",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, ident);
}
}
- (void)testInitWithDictionaryInvalid {
@@ -123,4 +225,63 @@
XCTAssertNil(sut);
}
- (void)testRuleDictionaryRepresentation {
NSDictionary *expectedTeamID = @{
@"identifier" : @"ABCDEFGHIJ",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
@"custom_msg" : @"A custom block message",
@"custom_url" : @"https://example.com",
};
SNTRule *sut = [[SNTRule alloc] initWithDictionary:expectedTeamID];
NSDictionary *dict = [sut dictionaryRepresentation];
XCTAssertEqualObjects(expectedTeamID, dict);
NSDictionary *expectedBinary = @{
@"identifier" : @"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6",
@"policy" : @"BLOCKLIST",
@"rule_type" : @"BINARY",
@"custom_msg" : @"",
@"custom_url" : @"",
};
sut = [[SNTRule alloc] initWithDictionary:expectedBinary];
dict = [sut dictionaryRepresentation];
XCTAssertEqualObjects(expectedBinary, dict);
}
- (void)testRuleStateToPolicyString {
NSDictionary *expected = @{
@"identifier" : @"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"BINARY",
@"custom_msg" : @"A custom block message",
@"custom_url" : @"https://example.com",
};
SNTRule *sut = [[SNTRule alloc] initWithDictionary:expected];
sut.state = SNTRuleStateBlock;
XCTAssertEqualObjects(kRulePolicyBlocklist, [sut dictionaryRepresentation][kRulePolicy]);
sut.state = SNTRuleStateSilentBlock;
XCTAssertEqualObjects(kRulePolicySilentBlocklist, [sut dictionaryRepresentation][kRulePolicy]);
sut.state = SNTRuleStateAllow;
XCTAssertEqualObjects(kRulePolicyAllowlist, [sut dictionaryRepresentation][kRulePolicy]);
sut.state = SNTRuleStateAllowCompiler;
XCTAssertEqualObjects(kRulePolicyAllowlistCompiler, [sut dictionaryRepresentation][kRulePolicy]);
// Invalid states
sut.state = SNTRuleStateRemove;
XCTAssertEqualObjects(kRulePolicyRemove, [sut dictionaryRepresentation][kRulePolicy]);
}
/*
- (void)testRuleTypeToString {
SNTRule *sut = [[SNTRule alloc] init];
XCTAssertEqual(kRuleTypeBinary, [sut ruleTypeToString:@""]);//SNTRuleTypeBinary]);
XCTAssertEqual(kRuleTypeCertificate,[sut ruleTypeToString:SNTRuleTypeCertificate]);
XCTAssertEqual(kRuleTypeTeamID, [sut ruleTypeToString:SNTRuleTypeTeamID]);
XCTAssertEqual(kRuleTypeSigningID,[sut ruleTypeToString:SNTRuleTypeSigningID]);
}*/
@end

View File

@@ -54,6 +54,7 @@ extern NSString *const kEnableTransitiveRulesDeprecated;
extern NSString *const kEnableTransitiveRulesSuperDeprecated;
extern NSString *const kEnableAllEventUpload;
extern NSString *const kDisableUnknownEventUpload;
extern NSString *const kOverrideFileAccessAction;
extern NSString *const kEvents;
extern NSString *const kFileSHA256;
@@ -124,6 +125,7 @@ extern NSString *const kRuleTypeCertificate;
extern NSString *const kRuleTypeTeamID;
extern NSString *const kRuleTypeSigningID;
extern NSString *const kRuleCustomMsg;
extern NSString *const kRuleCustomURL;
extern NSString *const kCursor;
extern NSString *const kBackoffInterval;
@@ -135,6 +137,9 @@ extern NSString *const kLogSync;
extern const NSUInteger kDefaultEventBatchSize;
extern NSString *const kPostflightRulesReceived;
extern NSString *const kPostflightRulesProcessed;
///
/// kDefaultFullSyncInterval
/// kDefaultFCMFullSyncInterval

View File

@@ -47,6 +47,7 @@ NSString *const kFullSyncInterval = @"full_sync_interval";
NSString *const kFCMToken = @"fcm_token";
NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval";
NSString *const kFCMGlobalRuleSyncDeadline = @"fcm_global_rule_sync_deadline";
NSString *const kOverrideFileAccessAction = @"override_file_access_action";
NSString *const kEnableBundles = @"enable_bundles";
NSString *const kEnableBundlesDeprecated = @"bundles_enabled";
@@ -125,6 +126,7 @@ NSString *const kRuleTypeCertificate = @"CERTIFICATE";
NSString *const kRuleTypeTeamID = @"TEAMID";
NSString *const kRuleTypeSigningID = @"SIGNINGID";
NSString *const kRuleCustomMsg = @"custom_msg";
NSString *const kRuleCustomURL = @"custom_url";
NSString *const kCursor = @"cursor";
NSString *const kBackoffInterval = @"backoff";
@@ -134,6 +136,9 @@ NSString *const kRuleSync = @"rule_sync";
NSString *const kConfigSync = @"config_sync";
NSString *const kLogSync = @"log_sync";
NSString *const kPostflightRulesReceived = @"rules_received";
NSString *const kPostflightRulesProcessed = @"rules_processed";
const NSUInteger kDefaultEventBatchSize = 50;
const NSUInteger kDefaultFullSyncInterval = 600;
const NSUInteger kDefaultPushNotificationsFullSyncInterval = 14400;

View File

@@ -37,6 +37,7 @@
teamID:(NSString *)teamID
signingID:(NSString *)signingID
reply:(void (^)(SNTRule *))reply;
- (void)retrieveAllRules:(void (^)(NSArray<SNTRule *> *rules, NSError *error))reply;
///
/// Config ops
@@ -53,6 +54,7 @@
- (void)setEnableTransitiveRules:(BOOL)enabled reply:(void (^)(void))reply;
- (void)setEnableAllEventUpload:(BOOL)enabled reply:(void (^)(void))reply;
- (void)setDisableUnknownEventUpload:(BOOL)enabled reply:(void (^)(void))reply;
- (void)setOverrideFileAccessAction:(NSString *)action reply:(void (^)(void))reply;
///
/// Syncd Ops

View File

@@ -53,6 +53,11 @@ NSString *const kBundleID = @"com.google.santa.daemon";
forSelector:@selector(databaseRuleAddRules:cleanSlate:reply:)
argumentIndex:0
ofReply:NO];
[r setClasses:[NSSet setWithObjects:[NSArray class], [SNTRule class], nil]
forSelector:@selector(retrieveAllRules:)
argumentIndex:0
ofReply:YES];
}
+ (NSXPCInterface *)controlInterface {

View File

@@ -23,10 +23,14 @@
/// Protocol implemented by SantaGUI and utilized by santad
@protocol SNTNotifierXPC
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message;
- (void)postBlockNotification:(SNTStoredEvent *)event
withCustomMessage:(NSString *)message
andCustomURL:(NSString *)url;
- (void)postUSBBlockNotification:(SNTDeviceEvent *)event withCustomMessage:(NSString *)message;
- (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
withCustomMessage:(NSString *)message API_AVAILABLE(macos(13.0));
customMessage:(NSString *)message
customURL:(NSString *)url
customText:(NSString *)text API_AVAILABLE(macos(13.0));
- (void)postClientModeNotification:(SNTClientMode)clientmode;
- (void)postRuleSyncNotificationWithCustomMessage:(NSString *)message;
- (void)updateCountsForEvent:(SNTStoredEvent *)event

View File

@@ -17,6 +17,7 @@
#include <Foundation/Foundation.h>
#include <optional>
#include <string>
#include <string_view>
@@ -38,6 +39,15 @@ static inline NSString *StringToNSString(const char *str) {
return [NSString stringWithUTF8String:str];
}
static inline NSString *OptionalStringToNSString(const std::optional<std::string> &optional_str) {
std::string str = optional_str.value_or("");
if (str.length() == 0) {
return nil;
} else {
return StringToNSString(str);
}
}
} // namespace santa::common
#endif

View File

@@ -22,6 +22,8 @@
#include <gtest/gtest.h>
#include <sys/stat.h>
#include <string>
#define NOBODY_UID ((unsigned int)-2)
#define NOGROUP_GID ((unsigned int)-1)
@@ -38,6 +40,10 @@
// Pretty print C++ string match errors
#define XCTAssertCppStringEqual(got, want) XCTAssertCStringEqual((got).c_str(), (want).c_str())
#define XCTAssertCppStringBeginsWith(got, want) \
XCTAssertTrue((got).rfind((want), 0) == 0, "\nPrefix not found.\n\t got: %s\n\twant: %s\n", \
(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) \
@@ -49,15 +55,18 @@
// function returns early due to interrupts.
void SleepMS(long ms);
enum class ActionType {
Auth,
Notify,
};
// Helper to construct strings of a given length
NSString *RepeatedString(NSString *str, NSUInteger len);
//
// Helpers to construct various ES structs
//
enum class ActionType {
Auth,
Notify,
};
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver);
/// Construct a `struct stat` buffer with each member having a unique value.
@@ -67,7 +76,7 @@ audit_token_t MakeAuditToken(pid_t pid, pid_t pidver);
struct stat MakeStat(int offset = 0);
es_string_token_t MakeESStringToken(const char *s);
es_file_t MakeESFile(const char *path, struct stat sb = {});
es_file_t MakeESFile(const char *path, struct stat sb = MakeStat());
es_process_t MakeESProcess(es_file_t *file, audit_token_t tok = {}, audit_token_t parent_tok = {});
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc,
ActionType action_type = ActionType::Notify,

View File

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

View File

@@ -429,6 +429,9 @@ message Disk {
// Time device appeared/disappeared
optional google.protobuf.Timestamp appearance = 10;
// Path mounted from
optional string mount_from = 11;
}
// Information emitted when Santa captures bundle information

View File

@@ -79,6 +79,7 @@ objc_library(
":SNTAboutWindowView",
":SNTDeviceMessageWindowView",
":SNTFileAccessMessageWindowView",
"//Source/common:CertificateHelpers",
"//Source/common:SNTBlockMessage_SantaGUI",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDeviceEvent",

View File

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

View File

@@ -17,6 +17,7 @@
#import <MOLCertificate/MOLCertificate.h>
#import <SecurityInterface/SFCertificatePanel.h>
#import "Source/common/CertificateHelpers.h"
#import "Source/common/SNTBlockMessage.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTStoredEvent.h"
@@ -25,6 +26,9 @@
/// The custom message to display for this event
@property(copy) NSString *customMessage;
/// The custom URL to use for this event
@property(copy) NSString *customURL;
/// A 'friendly' string representing the certificate information
@property(readonly, nonatomic) NSString *publisherInfo;
@@ -39,11 +43,14 @@
@implementation SNTBinaryMessageWindowController
- (instancetype)initWithEvent:(SNTStoredEvent *)event andMessage:(NSString *)message {
- (instancetype)initWithEvent:(SNTStoredEvent *)event
customMsg:(NSString *)message
customURL:(NSString *)url {
self = [super initWithWindowNibName:@"MessageWindow"];
if (self) {
_event = event;
_customMessage = message;
_customURL = url;
_progress = [NSProgress discreteProgressWithTotalUnitCount:1];
[_progress addObserver:self
forKeyPath:@"fractionCompleted"
@@ -74,9 +81,14 @@
- (void)loadWindow {
[super loadWindow];
if (![[SNTConfigurator configurator] eventDetailURL]) {
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event customURL:self.customURL];
if (!url) {
[self.openEventButton removeFromSuperview];
} else {
} else if (self.customURL.length == 0) {
// Set the button text only if a per-rule custom URL is not used. If a
// custom URL is used, it is assumed that the `EventDetailText` config value
// does not apply and the default text will be used.
NSString *eventDetailText = [[SNTConfigurator configurator] eventDetailText];
if (eventDetailText) {
[self.openEventButton setTitle:eventDetailText];
@@ -106,21 +118,17 @@
- (IBAction)showCertInfo:(id)sender {
// SFCertificatePanel expects an NSArray of SecCertificateRef's
NSMutableArray *certArray = [NSMutableArray arrayWithCapacity:[self.event.signingChain count]];
for (MOLCertificate *cert in self.event.signingChain) {
[certArray addObject:(id)cert.certRef];
}
[[[SFCertificatePanel alloc] init] beginSheetForWindow:self.window
modalDelegate:nil
didEndSelector:nil
contextInfo:nil
certificates:certArray
certificates:CertificateChain(self.event.signingChain)
showGroup:YES];
}
- (IBAction)openEventDetails:(id)sender {
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event];
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event customURL:self.customURL];
[self closeWindow:sender];
[[NSWorkspace sharedWorkspace] openURL:url];
}
@@ -136,19 +144,7 @@
}
- (NSString *)publisherInfo {
MOLCertificate *leafCert = [self.event.signingChain firstObject];
if ([leafCert.commonName isEqualToString:@"Apple Mac OS Application Signing"]) {
return [NSString stringWithFormat:@"App Store (Team ID: %@)", self.event.teamID];
} else if (leafCert.commonName && leafCert.orgName) {
return [NSString stringWithFormat:@"%@ - %@", leafCert.orgName, leafCert.commonName];
} else if (leafCert.commonName) {
return leafCert.commonName;
} else if (leafCert.orgName) {
return leafCert.orgName;
} else {
return nil;
}
return Publisher(self.event.signingChain, self.event.teamID);
}
- (NSAttributedString *)attributedCustomMessage {

View File

@@ -26,7 +26,10 @@ NS_ASSUME_NONNULL_BEGIN
API_AVAILABLE(macos(13.0))
@interface SNTFileAccessMessageWindowController : SNTMessageWindowController <NSWindowDelegate>
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event message:(nullable NSString *)message;
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event
customMessage:(nullable NSString *)message
customURL:(nullable NSString *)url
customText:(nullable NSString *)text;
@property(readonly) SNTFileAccessEvent *event;

View File

@@ -21,16 +21,23 @@
@interface SNTFileAccessMessageWindowController ()
@property NSString *customMessage;
@property NSString *customURL;
@property NSString *customText;
@property SNTFileAccessEvent *event;
@end
@implementation SNTFileAccessMessageWindowController
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event message:(nullable NSString *)message {
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event
customMessage:(nullable NSString *)message
customURL:(nullable NSString *)url
customText:(nullable NSString *)text {
self = [super init];
if (self) {
_customMessage = message;
_event = event;
_customMessage = message;
_customURL = url;
_customText = text;
}
return self;
}
@@ -40,21 +47,27 @@
[self.window orderOut:sender];
}
self.window =
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
styleMask:NSWindowStyleMaskClosable | NSWindowStyleMaskTitled
backing:NSBackingStoreBuffered
defer:NO];
self.window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
styleMask:NSWindowStyleMaskBorderless
backing:NSBackingStoreBuffered
defer:NO];
self.window.contentViewController =
[SNTFileAccessMessageWindowViewFactory createWithWindow:self.window
event:self.event
customMsg:self.attributedCustomMessage];
self.window.contentViewController = [SNTFileAccessMessageWindowViewFactory
createWithWindow:self.window
event:self.event
customMessage:self.attributedCustomMessage
customURL:[SNTBlockMessage eventDetailURLForFileAccessEvent:self.event
customURL:self.customURL]
.absoluteString
customText:self.customText
uiStateCallback:^(BOOL preventNotificationsForADay) {
self.silenceFutureNotifications = preventNotificationsForADay;
}];
self.window.delegate = self;
// Add app to Cmd+Tab and Dock.
NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
// Make sure app doesn't appear in Cmd+Tab or Dock.
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
[super showWindow:sender];
}
@@ -66,14 +79,17 @@
}
- (NSAttributedString *)attributedCustomMessage {
return [SNTBlockMessage formatMessage:self.customMessage];
return [SNTBlockMessage attributedBlockMessageForFileAccessEvent:self.event
customMessage: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]];
// The hash for display de-duplication/silencing purposes is a combination of:
// 1. The current file access rule version
// 2. The name of the rule that was violated
// 3. The path of the process
return [NSString
stringWithFormat:@"%@|%@|%@", self.event.ruleVersion, self.event.ruleName, self.event.filePath];
}
@end

View File

@@ -12,14 +12,25 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
import SecurityInterface
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)
@objc public static func createWith(window: NSWindow,
event: SNTFileAccessEvent,
customMessage: NSAttributedString?,
customURL: NSString?,
customText: NSString?,
uiStateCallback: ((Bool) -> Void)?) -> NSViewController {
return NSHostingController(rootView:SNTFileAccessMessageWindowView(window:window,
event:event,
customMessage:customMessage,
customURL:customURL as String?,
customText:customText as String?,
uiStateCallback:uiStateCallback)
.frame(width:800, height:600))
}
}
@@ -28,16 +39,26 @@ import santa_common_SNTFileAccessEvent
struct Property : View {
var lbl: String
var val: String
var propertyAction: (() -> Void)? = nil
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)
HStack {
if let block = propertyAction {
Button(action: {
block()
}) {
Image(systemName: "info.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
}
Text(lbl + ":")
.frame(alignment: .trailing)
.lineLimit(1)
.font(.system(size: 12, weight: .bold))
.padding(Edge.Set.horizontal, 10)
}.frame(width: width, alignment: .trailing)
Text(val)
.fixedSize(horizontal: false, vertical: true)
@@ -50,6 +71,7 @@ struct Property : View {
@available(macOS 13, *)
struct Event: View {
let e: SNTFileAccessEvent
let window: NSWindow?
var body: some View {
VStack(spacing:10) {
@@ -64,6 +86,18 @@ struct Event: View {
Property(lbl: "Application", val: app)
}
if let pub = e.publisherInfo {
Property(lbl: "Publisher", val: pub) {
SFCertificatePanel.shared()
.beginSheet(for: window,
modalDelegate: nil,
didEnd: nil,
contextInfo: nil,
certificates: e.signingChainCertRefs,
showGroup: true)
}
}
Property(lbl: "Name", val: (e.filePath as NSString).lastPathComponent)
Property(lbl: "Path", val: e.filePath)
Property(lbl: "Identifier", val: e.fileSHA256)
@@ -76,22 +110,26 @@ struct Event: View {
struct SNTFileAccessMessageWindowView: View {
let window: NSWindow?
let event: SNTFileAccessEvent?
let customMsg: NSAttributedString?
let customMessage: NSAttributedString?
let customURL: String?
let customText: String?
let uiStateCallback: ((Bool) -> Void)?
@State private var checked = false
@Environment(\.openURL) var openURL
@State public 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 {
if let msg = customMessage {
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!)
Event(e: event!, window: window)
Toggle(isOn: $checked) {
Text("Prevent future notifications for this application for a day")
@@ -99,9 +137,12 @@ struct SNTFileAccessMessageWindowView: View {
}
VStack(spacing:15) {
Button(action: openButton, label: {
Text("Open Event Info...").frame(maxWidth:.infinity)
})
if customURL != nil {
Button(action: openButton, label: {
Text(customText ?? "Open Event...").frame(maxWidth:.infinity)
})
}
Button(action: dismissButton, label: {
Text("Dismiss").frame(maxWidth:.infinity)
})
@@ -113,19 +154,25 @@ struct SNTFileAccessMessageWindowView: View {
}.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...")
guard let urlString = customURL else {
print("No URL available")
return
}
guard let url = URL(string: urlString) else {
print("Failed to create URL")
return
}
openURL(url)
}
func dismissButton() {
if let block = uiStateCallback {
block(self.checked)
}
window?.close()
print("close window")
}
}
@@ -153,6 +200,11 @@ func testFileAccessEvent() -> SNTFileAccessEvent {
@available(macOS 13, *)
struct SNTFileAccessMessageWindowView_Previews: PreviewProvider {
static var previews: some View {
SNTFileAccessMessageWindowView(window: nil, event: testFileAccessEvent(), customMsg: nil)
SNTFileAccessMessageWindowView(window: nil,
event: testFileAccessEvent(),
customMessage: nil,
customURL: nil,
customText: nil,
uiStateCallback: nil)
}
}

View File

@@ -172,7 +172,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
[dc postNotificationName:@"com.google.santa.notification.blockedeexecution"
object:@"com.google.santa"
userInfo:userInfo];
userInfo:userInfo
deliverImmediately:YES];
}
- (void)showQueuedWindow {
@@ -322,14 +323,16 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
[un addNotificationRequest:req withCompletionHandler:nil];
}
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message {
- (void)postBlockNotification:(SNTStoredEvent *)event
withCustomMessage:(NSString *)message
andCustomURL:(NSString *)url {
if (!event) {
LOGI(@"Error: Missing event object in message received from daemon!");
return;
}
SNTBinaryMessageWindowController *pendingMsg =
[[SNTBinaryMessageWindowController alloc] initWithEvent:event andMessage:message];
[[SNTBinaryMessageWindowController alloc] initWithEvent:event customMsg:message customURL:url];
[self queueMessage:pendingMsg];
}
@@ -346,14 +349,19 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
}
- (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
withCustomMessage:(NSString *)message API_AVAILABLE(macos(13.0)) {
customMessage:(NSString *)message
customURL:(NSString *)url
customText:(NSString *)text 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];
[[SNTFileAccessMessageWindowController alloc] initWithEvent:event
customMessage:message
customURL:url
customText:text];
[self queueMessage:pendingMsg];
}

View File

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

View File

@@ -31,6 +31,7 @@ objc_library(
"//Source/common:SNTLogging",
"//Source/common:santa_cc_proto_library_wrapper",
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:binaryproto_cc_proto_library_wrapper",
"@com_google_protobuf//src/google/protobuf/json",
],
)

View File

@@ -13,7 +13,7 @@
/// limitations under the License.
#import <Foundation/Foundation.h>
#include <google/protobuf/util/json_util.h>
#include <google/protobuf/json/json.h>
#include <stdlib.h>
#include <iostream>
@@ -26,8 +26,8 @@
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/binaryproto_proto_include_wrapper.h"
#include "google/protobuf/any.pb.h"
using google::protobuf::util::JsonPrintOptions;
using google::protobuf::util::MessageToJsonString;
using JsonPrintOptions = google::protobuf::json::PrintOptions;
using google::protobuf::json::MessageToJsonString;
using santa::fsspool::binaryproto::LogBatch;
namespace pbv1 = ::santa::pb::v1;

View File

@@ -54,6 +54,8 @@ REGISTER_COMMAND_NAME(@"rule")
@" --compiler: allow and mark as a compiler\n"
@" --remove: remove existing rule\n"
@" --check: check for an existing rule\n"
@" --import: import rules from a JSON file\n"
@" --export: export rules to a JSON file\n"
@"\n"
@" One of:\n"
@" --path {path}: path of binary/bundle to add/remove.\n"
@@ -62,6 +64,7 @@ REGISTER_COMMAND_NAME(@"rule")
@" the rule state of a file.\n"
@" --identifier {sha256|teamID|signingID}: identifier to add/remove/check\n"
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
@" --json {path}: path to a JSON file containing a list of rules to add/remove\n"
@"\n"
@" Optionally:\n"
@" --teamid: add or check a team ID rule instead of binary\n"
@@ -81,7 +84,21 @@ REGISTER_COMMAND_NAME(@"rule")
@" 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");
@" targeting Apple-signed binaries that do not have a team ID.\n"
@"\n"
@" Importing / Exporting Rules:\n"
@" If santa is not configured to use a sync server one can export\n"
@" & import its non-static rules to and from JSON files using the \n"
@" --export/--import flags. These files have the following form:\n"
@"\n"
@" {\"rules\": [{rule-dictionaries}]}\n"
@" e.g. {\"rules\": [\n"
@" {\"policy\": \"BLOCKLIST\",\n"
@" \"identifier\": "
@"\"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6\"\n"
@" \"custom_url\" : \"\",\n"
@" \"custom_msg\": \"/bin/ls block for demo\"}\n"
@" ]}\n");
}
- (void)runWithArguments:(NSArray *)arguments {
@@ -103,7 +120,10 @@ REGISTER_COMMAND_NAME(@"rule")
newRule.type = SNTRuleTypeBinary;
NSString *path;
NSString *jsonFilePath;
BOOL check = NO;
BOOL importRules = NO;
BOOL exportRules = NO;
// Parse arguments
for (NSUInteger i = 0; i < arguments.count; ++i) {
@@ -154,6 +174,33 @@ REGISTER_COMMAND_NAME(@"rule")
} else if ([arg caseInsensitiveCompare:@"--force"] == NSOrderedSame) {
// Don't do anything special.
#endif
} else if ([arg caseInsensitiveCompare:@"--json"] == NSOrderedSame) {
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--json requires an argument"];
}
jsonFilePath = arguments[i];
} else if ([arg caseInsensitiveCompare:@"--import"] == NSOrderedSame) {
if (exportRules) {
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
}
importRules = YES;
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--import requires an argument"];
}
jsonFilePath = arguments[i];
} else if ([arg caseInsensitiveCompare:@"--export"] == NSOrderedSame) {
if (importRules) {
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
}
exportRules = YES;
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--export requires an argument"];
}
jsonFilePath = arguments[i];
} else if ([arg caseInsensitiveCompare:@"--help"] == NSOrderedSame ||
[arg caseInsensitiveCompare:@"-h"] == NSOrderedSame) {
printf("%s\n", self.class.longHelpText.UTF8String);
exit(0);
} else {
[self printErrorUsageAndExit:[@"Unknown argument: " stringByAppendingString:arg]];
}
@@ -189,6 +236,16 @@ REGISTER_COMMAND_NAME(@"rule")
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
}
// Note this block needs to come after the check block above.
if (jsonFilePath.length > 0) {
if (importRules) {
[self importJSONFile:jsonFilePath];
} else if (exportRules) {
[self exportJSONFile:jsonFilePath];
}
return;
}
if (newRule.state == SNTRuleStateUnknown) {
[self printErrorUsageAndExit:@"No state specified"];
} else if (!newRule.identifier) {
@@ -302,4 +359,101 @@ REGISTER_COMMAND_NAME(@"rule")
exit(0);
}
- (void)importJSONFile:(NSString *)jsonFilePath {
// If the file exists parse it and then add the rules one at a time.
NSError *error;
NSData *data = [NSData dataWithContentsOfFile:jsonFilePath options:0 error:&error];
if (error) {
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Failed to read %@: %@", jsonFilePath,
error.localizedDescription]];
}
// We expect a JSON object with one key "rules". This is an array of rule
// objects.
// e.g.
// {"rules": [{
// "policy" : "BLOCKLIST",
// "rule_type" : "BINARY",
// "identifier" : "84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6"
// "custom_url" : "",
// "custom_msg" : "/bin/ls block for demo"
// }]}
NSDictionary *rules = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error) {
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Failed to parse %@: %@", jsonFilePath,
error.localizedDescription]];
}
NSMutableArray<SNTRule *> *parsedRules = [[NSMutableArray alloc] init];
for (NSDictionary *jsonRule in rules[@"rules"]) {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:jsonRule];
if (!rule) {
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Invalid rule: %@", jsonRule]];
}
[parsedRules addObject:rule];
}
[[self.daemonConn remoteObjectProxy]
databaseRuleAddRules:parsedRules
cleanSlate:NO
reply:^(NSError *error) {
if (error) {
printf("Failed to modify rules: %s",
[error.localizedDescription UTF8String]);
LOGD(@"Failure reason: %@", error.localizedFailureReason);
exit(1);
}
exit(0);
}];
}
- (void)exportJSONFile:(NSString *)jsonFilePath {
// Get the rules from the daemon and then write them to the file.
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
[rop retrieveAllRules:^(NSArray<SNTRule *> *rules, NSError *error) {
if (error) {
printf("Failed to get rules: %s", [error.localizedDescription UTF8String]);
LOGD(@"Failure reason: %@", error.localizedFailureReason);
exit(1);
}
if (rules.count == 0) {
printf("No rules to export.\n");
exit(1);
}
// Convert Rules to an NSDictionary.
NSMutableArray *rulesAsDicts = [[NSMutableArray alloc] init];
for (SNTRule *rule in rules) {
// Omit transitive and remove rules as they're not relevan.
if (rule.state == SNTRuleStateAllowTransitive || rule.state == SNTRuleStateRemove) {
continue;
}
[rulesAsDicts addObject:[rule dictionaryRepresentation]];
}
NSOutputStream *outputStream = [[NSOutputStream alloc] initToFileAtPath:jsonFilePath append:NO];
[outputStream open];
// Write the rules to the file.
// File should look like the following JSON:
// {"rules": [{"policy": "ALLOWLIST", "identifier": hash, "rule_type: "BINARY"},}]}
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:@{@"rules" : rulesAsDicts}
options:NSJSONWritingPrettyPrinted
error:&error];
// Print error
if (error) {
printf("Failed to jsonify rules: %s", [error.localizedDescription UTF8String]);
LOGD(@"Failure reason: %@", error.localizedFailureReason);
exit(1);
}
// Write jsonData to the file
[outputStream write:jsonData.bytes maxLength:jsonData.length];
[outputStream close];
exit(0);
}];
}
@end

View File

@@ -213,6 +213,9 @@ objc_library(
name = "TTYWriter",
srcs = ["TTYWriter.mm"],
hdrs = ["TTYWriter.h"],
sdk_dylibs = [
"EndpointSecurity",
],
deps = [
"//Source/common:SNTLogging",
"//Source/common:String",
@@ -359,9 +362,11 @@ objc_library(
":SNTDecisionCache",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
":TTYWriter",
":WatchItemPolicy",
":WatchItems",
"//Source/common:Platform",
"//Source/common:SNTBlockMessage",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTFileAccessEvent",
@@ -373,6 +378,8 @@ objc_library(
"//Source/common:String",
"@MOLCertificate",
"@MOLCodesignChecker",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/container:flat_hash_set",
],
)
@@ -467,6 +474,7 @@ objc_library(
"//Source/common:SantaCache",
"//Source/common:SantaVnode",
"//Source/common:SantaVnodeHash",
"//Source/common:String",
],
)
@@ -524,6 +532,8 @@ objc_library(
"//Source/common:SNTStoredEvent",
"//Source/common:String",
"//Source/common:santa_cc_proto_library_wrapper",
"@com_google_absl//absl/status",
"@com_google_protobuf//src/google/protobuf/json",
],
)
@@ -664,6 +674,7 @@ objc_library(
hdrs = ["Metrics.h"],
deps = [
":SNTApplicationCoreMetrics",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SNTXPCMetricServiceInterface",
@@ -692,6 +703,7 @@ objc_library(
":SNTExecutionController",
":SNTNotificationQueue",
":SNTSyncdQueue",
":TTYWriter",
":WatchItems",
"//Source/common:PrefixTree",
"//Source/common:SNTCommonEnums",
@@ -881,8 +893,10 @@ santa_unit_test(
":Metrics",
":MockEndpointSecurityAPI",
":SNTDatabaseController",
":SNTDecisionCache",
":SNTEndpointSecurityAuthorizer",
":SantadDeps",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTConfigurator",
"//Source/common:TestUtils",
"@MOLCertificate",
@@ -981,7 +995,9 @@ santa_unit_test(
"//Source/common:TestUtils",
"//Source/common:santa_cc_proto_library_wrapper",
"@OCMock",
"@com_google_absl//absl/status",
"@com_google_googletest//:gtest",
"@com_google_protobuf//src/google/protobuf/json",
],
)
@@ -1230,6 +1246,7 @@ santa_unit_test(
":WatchItems",
"//Source/common:Platform",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:TestUtils",
"@MOLCertificate",

View File

@@ -101,6 +101,11 @@
///
- (void)removeOutdatedTransitiveRules;
///
/// Retrieve all rules from the database for export.
///
- (NSArray<SNTRule *> *)retrieveAllRules;
///
/// A map of a file hashes to cached decisions. This is used to pre-validate and whitelist
/// certain critical system binaries that are integral to Santa's functionality.

View File

@@ -25,7 +25,7 @@
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTRule.h"
static const uint32_t kRuleTableCurrentVersion = 5;
static const uint32_t kRuleTableCurrentVersion = 7;
// TODO(nguyenphillip): this should be configurable.
// How many rules must be in database before we start trying to remove transitive rules.
@@ -229,6 +229,28 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
newVersion = 5;
}
if (version < 6) {
// Force hash identifiers for Binary and Certificate rules to always be lowercase
[db executeUpdate:@"UPDATE rules SET identifier = LOWER(identifier) WHERE type = ? OR type = ?",
@(SNTRuleTypeBinary), @(SNTRuleTypeCertificate)];
// Force team ID identifiers for TeamID rules to always be uppercase
[db executeUpdate:@"UPDATE rules SET identifier = UPPER(identifier) WHERE type = ?",
@(SNTRuleTypeTeamID)];
// Note: Intentionally not attempting to migrate exsting SigningID rules to enforce
// the TeamID component to be uppercase. Since this is a newer rule type, it is
// assumed to be unnecessary and we'd rather not maintain the SQL to perform this
// migration automatically.
newVersion = 6;
}
if (version < 7) {
[db executeUpdate:@"ALTER TABLE 'rules' ADD 'customurl' TEXT"];
newVersion = 7;
}
// Save signing info for launchd and santad. Used to ensure they are always allowed.
self.santadCSInfo = [[MOLCodesignChecker alloc] initWithSelf];
self.launchdCSInfo = [[MOLCodesignChecker alloc] initWithPID:1];
@@ -292,11 +314,13 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
}
- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs {
return [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
state:[rs intForColumn:@"state"]
type:[rs intForColumn:@"type"]
customMsg:[rs stringForColumn:@"custommsg"]
timestamp:[rs intForColumn:@"timestamp"]];
SNTRule *r = [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
state:[rs intForColumn:@"state"]
type:[rs intForColumn:@"type"]
customMsg:[rs stringForColumn:@"custommsg"]
timestamp:[rs intForColumn:@"timestamp"]];
r.customURL = [rs stringForColumn:@"customurl"];
return r;
}
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
@@ -410,10 +434,10 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
}
} else {
if (![db executeUpdate:@"INSERT OR REPLACE INTO rules "
@"(identifier, state, type, custommsg, timestamp) "
@"VALUES (?, ?, ?, ?, ?);",
@"(identifier, state, type, custommsg, customurl, timestamp) "
@"VALUES (?, ?, ?, ?, ?, ?);",
rule.identifier, @(rule.state), @(rule.type), rule.customMsg,
@(rule.timestamp)]) {
rule.customURL, @(rule.timestamp)]) {
[self fillError:error
code:SNTRuleTableErrorInsertOrReplaceFailed
message:[db lastErrorMessage]];
@@ -515,4 +539,19 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
return YES;
}
#pragma mark Querying
// Retrieve all rules from the Database
- (NSArray<SNTRule *> *)retrieveAllRules {
NSMutableArray<SNTRule *> *rules = [NSMutableArray array];
[self inDatabase:^(FMDatabase *db) {
FMResultSet *rs = [db executeQuery:@"SELECT * FROM rules"];
while ([rs next]) {
[rules addObject:[self ruleFromResultSet:rs]];
}
[rs close];
}];
return rules;
}
@end

View File

@@ -36,7 +36,7 @@
- (SNTRule *)_exampleTeamIDRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"teamID";
r.identifier = @"ABCDEFGHIJ";
r.state = SNTRuleStateBlock;
r.type = SNTRuleTypeTeamID;
r.customMsg = @"A teamID rule";
@@ -48,11 +48,11 @@
if (isPlatformBinary) {
r.identifier = @"platform:signingID";
} else {
r.identifier = @"teamID:signingID";
r.identifier = @"ABCDEFGHIJ:signingID";
}
r.state = SNTRuleStateBlock;
r.type = SNTRuleTypeSigningID;
r.customMsg = @"A teamID rule";
r.customMsg = @"A signingID rule";
return r;
}
@@ -187,9 +187,9 @@
SNTRule *r = [self.sut ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:nil
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"teamID");
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(r.type, SNTRuleTypeTeamID);
XCTAssertEqual([self.sut teamIDRuleCount], 1);
@@ -211,12 +211,12 @@
XCTAssertEqual([self.sut signingIDRuleCount], 2);
SNTRule *r = [self.sut ruleForBinarySHA256:nil
signingID:@"teamID:signingID"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:nil
teamID:nil];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"teamID:signingID");
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
r = [self.sut ruleForBinarySHA256:nil
@@ -243,9 +243,9 @@
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
SNTRule *r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
signingID:@"teamID:signingID"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
@@ -253,9 +253,9 @@
r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
signingID:@"teamID:signingID"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"unknowncert"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
@@ -265,26 +265,26 @@
ruleForBinarySHA256:@"unknown"
signingID:@"unknown"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
r = [self.sut ruleForBinarySHA256:@"unknown"
signingID:@"teamID:signingID"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"unknown"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"teamID:signingID");
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
XCTAssertEqual(r.type, SNTRuleTypeSigningID, @"Implicit rule ordering failed (SigningID)");
r = [self.sut ruleForBinarySHA256:@"unknown"
signingID:@"unknown"
certificateSHA256:@"unknown"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"teamID");
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(r.type, SNTRuleTypeTeamID, @"Implicit rule ordering failed (TeamID)");
}
@@ -301,4 +301,25 @@
[[NSFileManager defaultManager] removeItemAtPath:dbPath error:NULL];
}
- (void)testRetrieveAllRulesWithEmptyDatabase {
NSArray<SNTRule *> *rules = [self.sut retrieveAllRules];
XCTAssertEqual(rules.count, 0);
}
- (void)testRetrieveAllRulesWithMultipleRules {
[self.sut addRules:@[
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
[self _exampleSigningIDRuleIsPlatform:NO]
]
cleanSlate:NO
error:nil];
NSArray<SNTRule *> *rules = [self.sut retrieveAllRules];
XCTAssertEqual(rules.count, 4);
XCTAssertEqualObjects(rules[0], [self _exampleCertRule]);
XCTAssertEqualObjects(rules[1], [self _exampleBinaryRule]);
XCTAssertEqualObjects(rules[2], [self _exampleTeamIDRule]);
XCTAssertEqualObjects(rules[3], [self _exampleSigningIDRuleIsPlatform:NO]);
}
@end

View File

@@ -15,6 +15,7 @@
#ifndef SANTA__SANTAD__DATALAYER_WATCHITEMPOLICY_H
#define SANTA__SANTAD__DATALAYER_WATCHITEMPOLICY_H
#import <Foundation/Foundation.h>
#include <Kernel/kern/cs_blobs.h>
#include <optional>
@@ -29,16 +30,17 @@ enum class WatchItemPathType {
kLiteral,
};
static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType =
WatchItemPathType::kLiteral;
static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType = WatchItemPathType::kLiteral;
static constexpr bool kWatchItemPolicyDefaultAllowReadAccess = false;
static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
static constexpr bool kWatchItemPolicyDefaultInvertProcessExceptions = false;
static constexpr bool kWatchItemPolicyDefaultEnableSilentMode = false;
static constexpr bool kWatchItemPolicyDefaultEnableSilentTTYMode = false;
struct WatchItemPolicy {
struct Process {
Process(std::string bp, std::string sid, std::string ti,
std::vector<uint8_t> cdh, std::string ch, std::optional<bool> pb)
Process(std::string bp, std::string sid, std::string ti, std::vector<uint8_t> cdh,
std::string ch, std::optional<bool> pb)
: binary_path(bp),
signing_id(sid),
team_id(ti),
@@ -47,13 +49,11 @@ struct WatchItemPolicy {
platform_binary(pb) {}
bool operator==(const Process &other) const {
return binary_path == other.binary_path &&
signing_id == other.signing_id && team_id == other.team_id &&
cdhash == other.cdhash &&
return binary_path == other.binary_path && signing_id == other.signing_id &&
team_id == other.team_id && cdhash == other.cdhash &&
certificate_sha256 == other.certificate_sha256 &&
platform_binary.has_value() == other.platform_binary.has_value() &&
platform_binary.value_or(false) ==
other.platform_binary.value_or(false);
platform_binary.value_or(false) == other.platform_binary.value_or(false);
}
bool operator!=(const Process &other) const { return !(*this == other); }
@@ -71,27 +71,34 @@ struct WatchItemPolicy {
bool ara = kWatchItemPolicyDefaultAllowReadAccess,
bool ao = kWatchItemPolicyDefaultAuditOnly,
bool ipe = kWatchItemPolicyDefaultInvertProcessExceptions,
std::vector<Process> procs = {})
bool esm = kWatchItemPolicyDefaultEnableSilentMode,
bool estm = kWatchItemPolicyDefaultEnableSilentTTYMode, std::string_view cm = "",
NSString *edu = nil, NSString *edt = nil, std::vector<Process> procs = {})
: name(n),
path(p),
path_type(pt),
allow_read_access(ara),
audit_only(ao),
invert_process_exceptions(ipe),
silent(esm),
silent_tty(estm),
custom_message(cm.length() == 0 ? std::nullopt : std::make_optional<std::string>(cm)),
// Note: Empty string considered valid for event_detail_url to allow rules
// overriding global setting in order to hide the button.
event_detail_url(edu == nil ? std::nullopt : std::make_optional<NSString *>(edu)),
event_detail_text(edt.length == 0 ? std::nullopt : std::make_optional<NSString *>(edt)),
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 &&
invert_process_exceptions == other.invert_process_exceptions &&
processes == other.processes;
// Note: custom_message, event_detail_url, and event_detail_text are not currently considered
// for equality purposes
return name == other.name && path == other.path && path_type == other.path_type &&
allow_read_access == other.allow_read_access && audit_only == other.audit_only &&
invert_process_exceptions == other.invert_process_exceptions && silent == other.silent &&
silent_tty == other.silent_tty && processes == other.processes;
}
bool operator!=(const WatchItemPolicy &other) const {
return !(*this == other);
}
bool operator!=(const WatchItemPolicy &other) const { return !(*this == other); }
std::string name;
std::string path;
@@ -99,10 +106,14 @@ struct WatchItemPolicy {
bool allow_read_access;
bool audit_only;
bool invert_process_exceptions;
bool silent;
bool silent_tty;
std::optional<std::string> custom_message;
std::optional<NSString *> event_detail_url;
std::optional<NSString *> event_detail_text;
std::vector<Process> processes;
// WIP - No current way to control via config
bool silent = true;
std::string version = "temp_version";
};

View File

@@ -40,6 +40,9 @@ extern NSString *const kWatchItemConfigKeyOptions;
extern NSString *const kWatchItemConfigKeyOptionsAllowReadAccess;
extern NSString *const kWatchItemConfigKeyOptionsAuditOnly;
extern NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions;
extern NSString *const kWatchItemConfigKeyOptionsEnableSilentMode;
extern NSString *const kWatchItemConfigKeyOptionsEnableSilentTTYMode;
extern NSString *const kWatchItemConfigKeyOptionsCustomMessage;
extern NSString *const kWatchItemConfigKeyProcesses;
extern NSString *const kWatchItemConfigKeyProcessesBinaryPath;
extern NSString *const kWatchItemConfigKeyProcessesCertificateSha256;
@@ -93,6 +96,9 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
std::optional<WatchItemsState> State();
std::pair<NSString *, NSString *> EventDetailLinkInfo(
const std::shared_ptr<WatchItemPolicy> &watch_item);
friend class santa::santad::data_layer::WatchItemsPeer;
private:
@@ -125,6 +131,8 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
std::string policy_version_ ABSL_GUARDED_BY(lock_);
std::set<id<SNTEndpointSecurityDynamicEventHandler>> registerd_clients_ ABSL_GUARDED_BY(lock_);
bool periodic_task_started_ = false;
NSString *policy_event_detail_url_ ABSL_GUARDED_BY(lock_);
NSString *policy_event_detail_text_ ABSL_GUARDED_BY(lock_);
};
} // namespace santa::santad::data_layer

View File

@@ -46,6 +46,8 @@ using santa::santad::data_layer::WatchItemPathType;
using santa::santad::data_layer::WatchItemPolicy;
NSString *const kWatchItemConfigKeyVersion = @"Version";
NSString *const kWatchItemConfigKeyEventDetailURL = @"EventDetailURL";
NSString *const kWatchItemConfigKeyEventDetailText = @"EventDetailText";
NSString *const kWatchItemConfigKeyWatchItems = @"WatchItems";
NSString *const kWatchItemConfigKeyPaths = @"Paths";
NSString *const kWatchItemConfigKeyPathsPath = @"Path";
@@ -54,6 +56,11 @@ NSString *const kWatchItemConfigKeyOptions = @"Options";
NSString *const kWatchItemConfigKeyOptionsAllowReadAccess = @"AllowReadAccess";
NSString *const kWatchItemConfigKeyOptionsAuditOnly = @"AuditOnly";
NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions = @"InvertProcessExceptions";
NSString *const kWatchItemConfigKeyOptionsEnableSilentMode = @"EnableSilentMode";
NSString *const kWatchItemConfigKeyOptionsEnableSilentTTYMode = @"EnableSilentTTYMode";
NSString *const kWatchItemConfigKeyOptionsCustomMessage = @"BlockMessage";
NSString *const kWatchItemConfigKeyOptionsEventDetailURL = kWatchItemConfigKeyEventDetailURL;
NSString *const kWatchItemConfigKeyOptionsEventDetailText = kWatchItemConfigKeyEventDetailText;
NSString *const kWatchItemConfigKeyProcesses = @"Processes";
NSString *const kWatchItemConfigKeyProcessesBinaryPath = @"BinaryPath";
NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha256";
@@ -73,6 +80,22 @@ static constexpr NSUInteger kMaxSigningIDLength = 512;
// churn rebuilding glob paths based on the state of the filesystem.
static constexpr uint64_t kMinReapplyConfigFrequencySecs = 15;
// Semi-arbitrary max custom message length. The goal is to protect against
// potential unbounded lengths, but no real reason this cannot be higher.
static constexpr NSUInteger kWatchItemConfigOptionCustomMessageMaxLength = 2048;
// Semi-arbitrary max event detail text length. The text has to fit on a button
// and shouldn't be too large.
static constexpr NSUInteger kWatchItemConfigEventDetailTextMaxLength = 48;
// Servers are recommended to support up to 8000 octets.
// https://www.rfc-editor.org/rfc/rfc9110#section-4.1-5
//
// Seems excessive but no good reason to not allow long URLs. However because
// the URL supports pseudo-format strings that can extend the length, a smaller
// max is used here.
static constexpr NSUInteger kWatchItemConfigEventDetailURLMaxLength = 6000;
namespace santa::santad::data_layer {
// Type aliases
@@ -126,6 +149,10 @@ static std::vector<uint8_t> HexStringToBytes(NSString *str) {
return bytes;
}
static inline bool GetBoolValue(NSDictionary *options, NSString *key, bool default_value) {
return options[key] ? [options[key] boolValue] : default_value;
}
// Given a length, returns a ValidatorBlock that confirms the
// string is a valid hex string of the given length.
ValidatorBlock HexValidator(NSUInteger expected_length) {
@@ -140,8 +167,8 @@ ValidatorBlock HexValidator(NSUInteger expected_length) {
};
}
// Given a max length, returns a ValidatorBlock that confirms the
// string is a not longer than the max.
// Given a min and max length, returns a ValidatorBlock that confirms the
// string is within the given bounds.
ValidatorBlock LenRangeValidator(NSUInteger min_length, NSUInteger max_length) {
return ^bool(NSString *val, NSError **err) {
if (val.length < min_length) {
@@ -379,6 +406,16 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
/// <false/>
/// <key>InvertProcessExceptions</key>
/// <false/>
/// <key>EnableSilentMode</key>
/// <true/>
/// <key>EnableSilentTTYMode</key>
/// <true/>
/// <key>BlockMessage</key>
/// <string>...</string>
/// <key>EventDetailURL</key>
/// <string>...</string>
/// <key>EventDetailText</key>
/// <string>...</string>
/// </dict>
/// <key>Processes</key>
/// <array>
@@ -405,31 +442,48 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
NSDictionary *options = watch_item[kWatchItemConfigKeyOptions];
if (options) {
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAllowReadAccess, [NSNumber class],
err)) {
NSArray<NSString *> *boolOptions = @[
kWatchItemConfigKeyOptionsAllowReadAccess,
kWatchItemConfigKeyOptionsAuditOnly,
kWatchItemConfigKeyOptionsInvertProcessExceptions,
kWatchItemConfigKeyOptionsEnableSilentMode,
kWatchItemConfigKeyOptionsEnableSilentTTYMode,
];
for (NSString *key in boolOptions) {
if (!VerifyConfigKey(options, key, [NSNumber class], err)) {
return false;
}
}
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsCustomMessage, [NSString class], err,
false,
LenRangeValidator(0, kWatchItemConfigOptionCustomMessageMaxLength))) {
return false;
}
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAuditOnly, [NSNumber class], err)) {
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsEventDetailURL, [NSString class], err,
false, LenRangeValidator(0, kWatchItemConfigEventDetailURLMaxLength))) {
return false;
}
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsInvertProcessExceptions,
[NSNumber class], err)) {
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsEventDetailText, [NSString class], err,
false, LenRangeValidator(0, kWatchItemConfigEventDetailTextMaxLength))) {
return false;
}
}
bool allow_read_access = options[kWatchItemConfigKeyOptionsAllowReadAccess]
? [options[kWatchItemConfigKeyOptionsAllowReadAccess] boolValue]
: kWatchItemPolicyDefaultAllowReadAccess;
bool audit_only = options[kWatchItemConfigKeyOptionsAuditOnly]
? [options[kWatchItemConfigKeyOptionsAuditOnly] boolValue]
: kWatchItemPolicyDefaultAuditOnly;
bool allow_read_access = GetBoolValue(options, kWatchItemConfigKeyOptionsAllowReadAccess,
kWatchItemPolicyDefaultAllowReadAccess);
bool audit_only =
GetBoolValue(options, kWatchItemConfigKeyOptionsAuditOnly, kWatchItemPolicyDefaultAuditOnly);
bool invert_process_exceptions =
options[kWatchItemConfigKeyOptionsInvertProcessExceptions]
? [options[kWatchItemConfigKeyOptionsInvertProcessExceptions] boolValue]
: kWatchItemPolicyDefaultInvertProcessExceptions;
GetBoolValue(options, kWatchItemConfigKeyOptionsInvertProcessExceptions,
kWatchItemPolicyDefaultInvertProcessExceptions);
bool enable_silent_mode = GetBoolValue(options, kWatchItemConfigKeyOptionsEnableSilentMode,
kWatchItemPolicyDefaultEnableSilentMode);
bool enable_silent_tty_mode = GetBoolValue(options, kWatchItemConfigKeyOptionsEnableSilentTTYMode,
kWatchItemPolicyDefaultEnableSilentTTYMode);
std::variant<Unit, ProcessList> proc_list = VerifyConfigWatchItemProcesses(watch_item, err);
if (std::holds_alternative<Unit>(proc_list)) {
@@ -439,7 +493,11 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
for (const PathAndTypePair &path_type_pair : std::get<PathList>(path_list)) {
policies.push_back(std::make_shared<WatchItemPolicy>(
NSStringToUTF8StringView(name), path_type_pair.first, path_type_pair.second,
allow_read_access, audit_only, invert_process_exceptions, std::get<ProcessList>(proc_list)));
allow_read_access, audit_only, invert_process_exceptions, enable_silent_mode,
enable_silent_tty_mode,
NSStringToUTF8StringView(options[kWatchItemConfigKeyOptionsCustomMessage]),
options[kWatchItemConfigKeyOptionsEventDetailURL],
options[kWatchItemConfigKeyOptionsEventDetailText], std::get<ProcessList>(proc_list)));
}
return true;
@@ -487,6 +545,16 @@ bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPoli
return false;
}
if (!VerifyConfigKey(config, kWatchItemConfigKeyEventDetailURL, [NSString class], err, false,
LenRangeValidator(0, kWatchItemConfigEventDetailURLMaxLength))) {
return false;
}
if (!VerifyConfigKey(config, kWatchItemConfigKeyEventDetailText, [NSString class], err, false,
LenRangeValidator(0, kWatchItemConfigEventDetailTextMaxLength))) {
return false;
}
if (config[kWatchItemConfigKeyWatchItems] &&
![config[kWatchItemConfigKeyWatchItems] isKindOfClass:[NSDictionary class]]) {
PopulateError(err, [NSString stringWithFormat:@"Top level key '%@' must be a dictionary",
@@ -661,8 +729,18 @@ void WatchItems::UpdateCurrentState(
current_config_ = new_config;
if (new_config) {
policy_version_ = NSStringToUTF8String(new_config[kWatchItemConfigKeyVersion]);
// Non-existent kWatchItemConfigKeyEventDetailURL key or zero length value
// will both result in a nil global policy event detail URL.
if (((NSString *)new_config[kWatchItemConfigKeyEventDetailURL]).length) {
policy_event_detail_url_ = new_config[kWatchItemConfigKeyEventDetailURL];
} else {
policy_event_detail_url_ = nil;
}
policy_event_detail_text_ = new_config[kWatchItemConfigKeyEventDetailText];
} else {
policy_version_ = "";
policy_event_detail_url_ = nil;
policy_event_detail_text_ = nil;
}
last_update_time_ = [[NSDate date] timeIntervalSince1970];
@@ -794,4 +872,29 @@ std::optional<WatchItemsState> WatchItems::State() {
return state;
}
std::pair<NSString *, NSString *> WatchItems::EventDetailLinkInfo(
const std::shared_ptr<WatchItemPolicy> &watch_item) {
absl::ReaderMutexLock lock(&lock_);
if (!watch_item) {
return {policy_event_detail_url_, policy_event_detail_text_};
}
NSString *url = watch_item->event_detail_url.has_value() ? watch_item->event_detail_url.value()
: policy_event_detail_url_;
NSString *text = watch_item->event_detail_text.has_value() ? watch_item->event_detail_text.value()
: policy_event_detail_text_;
// Ensure empty strings are repplaced with nil
if (!url.length) {
url = nil;
}
if (!text.length) {
text = nil;
}
return {url, text};
}
} // namespace santa::santad::data_layer

View File

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

View File

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

View File

@@ -25,8 +25,10 @@
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/santad/Metrics.h"
#import "Source/santad/SNTDecisionCache.h"
#include "Source/santad/TTYWriter.h"
typedef void (^SNTFileAccessBlockCallback)(SNTFileAccessEvent *event);
typedef void (^SNTFileAccessBlockCallback)(SNTFileAccessEvent *event, NSString *customMsg,
NSString *customURL, NSString *customText);
@interface SNTEndpointSecurityFileAccessAuthorizer
: SNTEndpointSecurityClient <SNTEndpointSecurityDynamicEventHandler>
@@ -39,7 +41,8 @@ typedef void (^SNTFileAccessBlockCallback)(SNTFileAccessEvent *event);
watchItems:(std::shared_ptr<santa::santad::data_layer::WatchItems>)watchItems
enricher:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
decisionCache:(SNTDecisionCache *)decisionCache;
decisionCache:(SNTDecisionCache *)decisionCache
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter;
@property SNTFileAccessBlockCallback fileAccessBlockCallback;

View File

@@ -20,17 +20,20 @@
#import <MOLCodesignChecker/MOLCodesignChecker.h>
#include <bsm/libbsm.h>
#include <sys/fcntl.h>
#include <sys/types.h>
#include <algorithm>
#include <array>
#include <cstdlib>
#include <functional>
#include <memory>
#include <optional>
#include <set>
#include <type_traits>
#include <utility>
#include <variant>
#include "Source/common/Platform.h"
#import "Source/common/SNTBlockMessage.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#include "Source/common/SNTFileAccessEvent.h"
@@ -45,9 +48,15 @@
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/RateLimiter.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
using santa::common::OptionalStringToNSString;
using santa::common::StringToNSString;
using santa::santad::EventDisposition;
using santa::santad::FileAccessMetricStatus;
using santa::santad::Metrics;
using santa::santad::TTYWriter;
using santa::santad::data_layer::WatchItemPathType;
using santa::santad::data_layer::WatchItemPolicy;
using santa::santad::data_layer::WatchItems;
@@ -69,6 +78,122 @@ static constexpr uint16_t kDefaultRateLimitQPS = 50;
struct PathTarget {
std::string path;
bool isReadable;
std::optional<std::pair<dev_t, ino_t>> devnoIno;
};
// This is a bespoke cache for mapping processes to a set of values. It has
// similar semantics to SantaCache in terms of clearing the cache keys and
// values when max sizes are reached.
//
// TODO: We need a proper LRU cache
//
// NB: This exists instead of using SantaCache for two main reasons:
// 1.) SantaCache doesn't efficiently support non-primitive value types.
// Since the value of each key needs to be a set, we want to refrain
// from having to unnecessarily copy the value.
// 2.) SantaCache doesn't support size limits on value types
template <typename ValueT>
class ProcessSet {
using FileSet = absl::flat_hash_set<ValueT>;
public:
ProcessSet() {
q_ = dispatch_queue_create(
"com.google.santa.daemon.faa",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
QOS_CLASS_USER_INTERACTIVE, 0));
};
// Add the given target to the set of files a process can read
void Set(const es_process_t *proc, std::function<ValueT()> valueBlock) {
if (!valueBlock) {
return;
}
dispatch_sync(q_, ^{
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
audit_token_to_pidversion(proc->audit_token)};
SetLocked(pidPidver, valueBlock());
});
}
// Remove the given process from the cache
void Remove(const es_process_t *proc) {
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
audit_token_to_pidversion(proc->audit_token)};
dispatch_sync(q_, ^{
cache_.erase(pidPidver);
});
}
// Check if the set of files for a given process contains the given file
bool Exists(const es_process_t *proc, std::function<ValueT()> valueBlock) {
return ExistsOrSet(proc, valueBlock, false);
}
// Check if the ValueT set for a given process contains the given file, and
// if not, set it. Both steps are done atomically.
bool ExistsOrSet(const es_process_t *proc, std::function<ValueT()> valueBlock) {
return ExistsOrSet(proc, valueBlock, true);
}
// Clear all cache entries
void Clear() {
dispatch_sync(q_, ^{
ClearLocked();
});
}
private:
// Remove everything in the cache.
void ClearLocked() { cache_.clear(); }
void SetLocked(std::pair<pid_t, pid_t> pidPidver, ValueT value) {
// If we hit the size limit, clear the cache to prevent unbounded growth
if (cache_.size() >= kMaxCacheSize) {
ClearLocked();
}
FileSet &fs = cache_[std::move(pidPidver)];
// If we hit the per-entry size limit, clear the entry to prevent unbounded growth
if (fs.size() >= kMaxCacheEntrySize) {
fs.clear();
}
fs.insert(value);
}
bool ExistsOrSet(const es_process_t *proc, std::function<ValueT()> valueBlock, bool shouldSet) {
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
audit_token_to_pidversion(proc->audit_token)};
__block bool exists = false;
dispatch_sync(q_, ^{
ValueT value = valueBlock();
const auto &iter = cache_.find(pidPidver);
if (iter != cache_.end() && iter->second.count(value) > 0) {
exists = true;
} else if (shouldSet) {
SetLocked(pidPidver, value);
}
});
return exists;
}
dispatch_queue_t q_;
absl::flat_hash_map<std::pair<pid_t, pid_t>, FileSet> cache_;
// Cache limits are merely meant to protect against unbounded growth. In practice,
// the observed cache size is typically small for normal WatchItems rules (those
// that do not target high-volume paths). The per entry size was observed to vary
// quite dramatically based on the type of process (e.g. large, complex applications
// were observed to frequently have several thousands of entries).
static constexpr size_t kMaxCacheSize = 512;
static constexpr size_t kMaxCacheEntrySize = 8192;
};
static inline std::string Path(const es_file_t *esFile) {
@@ -82,14 +207,18 @@ static inline std::string Path(const es_string_token_t &tok) {
static inline void PushBackIfNotTruncated(std::vector<PathTarget> &vec, const es_file_t *esFile,
bool isReadable = false) {
if (!esFile->path_truncated) {
vec.push_back({Path(esFile), isReadable});
vec.push_back({Path(esFile), isReadable,
isReadable ? std::make_optional<std::pair<dev_t, ino_t>>(
{esFile->stat.st_dev, esFile->stat.st_ino})
: std::nullopt});
}
}
// Note: This variant of PushBackIfNotTruncated can never be marked "isReadable"
static inline void PushBackIfNotTruncated(std::vector<PathTarget> &vec, const es_file_t *dir,
const es_string_token_t &name, bool isReadable = false) {
const es_string_token_t &name) {
if (!dir->path_truncated) {
vec.push_back({Path(dir) + "/" + Path(name), isReadable});
vec.push_back({Path(dir) + "/" + Path(name), false, std::nullopt});
}
}
@@ -103,9 +232,43 @@ es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyDecision
case FileAccessPolicyDecision::kAllowedAuditOnly: return ES_AUTH_RESULT_ALLOW;
default:
// This is a programming error. Bail.
LOGE(@"Invalid file access decision encountered: %d", decision);
LOGE(@"Invalid file access decision encountered: %d", static_cast<int>(decision));
[NSException raise:@"Invalid FileAccessPolicyDecision"
format:@"Invalid FileAccessPolicyDecision: %d", decision];
format:@"Invalid FileAccessPolicyDecision: %d", static_cast<int>(decision)];
}
}
bool IsBlockDecision(FileAccessPolicyDecision decision) {
return decision == FileAccessPolicyDecision::kDenied ||
decision == FileAccessPolicyDecision::kDeniedInvalidSignature;
}
FileAccessPolicyDecision ApplyOverrideToDecision(FileAccessPolicyDecision decision,
SNTOverrideFileAccessAction overrideAction) {
switch (overrideAction) {
// When no override should be applied, return the decision unmodified
case SNTOverrideFileAccessActionNone: return decision;
// When the decision should be overridden to be audit only, only change the
// decision if it was going to deny the operation.
case SNTOverrideFileAccessActionAuditOnly:
if (IsBlockDecision(decision)) {
return FileAccessPolicyDecision::kAllowedAuditOnly;
} else {
return decision;
}
// If the override action is to disable policy, return a decision that will
// be treated as if no policy applied to the operation.
case SNTOverrideFileAccessActionDiable: return FileAccessPolicyDecision::kNoPolicy;
default:
// This is a programming error. Bail.
LOGE(@"Invalid override file access action encountered: %d",
static_cast<int>(overrideAction));
[NSException
raise:@"Invalid SNTOverrideFileAccessAction"
format:@"Invalid SNTOverrideFileAccessAction: %d", static_cast<int>(overrideAction)];
}
}
@@ -118,6 +281,10 @@ bool ShouldLogDecision(FileAccessPolicyDecision decision) {
}
}
bool ShouldNotifyUserDecision(FileAccessPolicyDecision decision) {
return ShouldLogDecision(decision) && decision != FileAccessPolicyDecision::kAllowedAuditOnly;
}
es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_result_t result2) {
// If either policy denied the operation, the operation is denied
return ((result1 == ES_AUTH_RESULT_DENY || result2 == ES_AUTH_RESULT_DENY)
@@ -196,7 +363,23 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
}
}
bool ShouldMessageTTY(const std::shared_ptr<WatchItemPolicy> &policy, const Message &msg,
ProcessSet<std::pair<std::string, std::string>> &ttyMessageCache) {
if (policy->silent_tty || !TTYWriter::CanWrite(msg->process)) {
return false;
}
// ExistsOrSet returns `true` if the item existed. However we want to invert
// this result as the return value for this function since we want to message
// the TTY only when `ExistsOrSet` was a "set" operation, meaning it was the
// first time this value was added.
return !ttyMessageCache.ExistsOrSet(msg->process, ^std::pair<std::string, std::string>() {
return {policy->version, policy->name};
});
}
@interface SNTEndpointSecurityFileAccessAuthorizer ()
@property SNTConfigurator *configurator;
@property SNTDecisionCache *decisionCache;
@property bool isSubscribed;
@end
@@ -207,17 +390,22 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
std::shared_ptr<Enricher> _enricher;
std::shared_ptr<RateLimiter> _rateLimiter;
SantaCache<SantaVnode, NSString *> _certHashCache;
std::shared_ptr<TTYWriter> _ttyWriter;
ProcessSet<std::pair<dev_t, ino_t>> _readsCache;
ProcessSet<std::pair<std::string, std::string>> _ttyMessageCache;
std::shared_ptr<Metrics> _metrics;
}
- (instancetype)
initWithESAPI:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
metrics:(std::shared_ptr<Metrics>)metrics
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
watchItems:(std::shared_ptr<WatchItems>)watchItems
enricher:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
decisionCache:(SNTDecisionCache *)decisionCache {
decisionCache:(SNTDecisionCache *)decisionCache
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter {
self = [super initWithESAPI:std::move(esApi)
metrics:metrics
processor:santa::santad::Processor::kFileAccessAuthorizer];
@@ -225,10 +413,13 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
_watchItems = std::move(watchItems);
_logger = std::move(logger);
_enricher = std::move(enricher);
_decisionCache = decisionCache;
_ttyWriter = std::move(ttyWriter);
_metrics = std::move(metrics);
_rateLimiter = RateLimiter::Create(metrics, santa::santad::Processor::kFileAccessAuthorizer,
_configurator = [SNTConfigurator configurator];
_rateLimiter = RateLimiter::Create(_metrics, santa::santad::Processor::kFileAccessAuthorizer,
kDefaultRateLimitQPS);
SNTMetricBooleanGauge *famEnabled = [[SNTMetricSet sharedInstance]
@@ -411,7 +602,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
// If the process is signed but has an invalid signature, it is denied
if (((msg->process->codesigning_flags & (CS_SIGNED | CS_VALID)) == CS_SIGNED) &&
[[SNTConfigurator configurator] enableBadSignatureProtection]) {
[self.configurator enableBadSignatureProtection]) {
// TODO(mlw): Think about how to make stronger guarantees here to handle
// programs becoming invalid after first being granted access. Maybe we
// should only allow things that have hardened runtime flags set?
@@ -420,6 +611,13 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
std::shared_ptr<WatchItemPolicy> policy = optionalPolicy.value();
// If policy allows reading, add target to the cache
if (policy->allow_read_access && target.devnoIno.has_value()) {
self->_readsCache.Set(msg->process, ^{
return *target.devnoIno;
});
}
// Check if this action contains any special case that would produce
// an immediate result.
FileAccessPolicyDecision specialCase = [self specialCaseForPolicy:policy
@@ -453,10 +651,6 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
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;
}
@@ -464,16 +658,25 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
target:(const PathTarget &)target
policy:
(std::optional<std::shared_ptr<WatchItemPolicy>>)optionalPolicy
policyVersion:(const std::string &)policyVersion {
FileAccessPolicyDecision policyDecision = [self applyPolicy:optionalPolicy
forTarget:target
toMessage:msg];
policyVersion:(const std::string &)policyVersion
overrideAction:(SNTOverrideFileAccessAction)overrideAction {
FileAccessPolicyDecision policyDecision = ApplyOverrideToDecision(
[self applyPolicy:optionalPolicy forTarget:target toMessage:msg], overrideAction);
// Note: If ShouldLogDecision, it shouldn't be possible for optionalPolicy
// to not have a value. Performing the check just in case to prevent a crash.
if (ShouldLogDecision(policyDecision) && optionalPolicy.has_value()) {
if (_rateLimiter->Decide(msg->mach_time) == RateLimiter::Decision::kAllowed) {
std::string policyNameCopy = optionalPolicy.value()->name;
std::shared_ptr<WatchItemPolicy> policy = optionalPolicy.value();
RateLimiter::Decision decision = _rateLimiter->Decide(msg->mach_time);
self->_metrics->SetFileAccessEventMetrics(policyVersion, policy->name,
(decision == RateLimiter::Decision::kAllowed)
? FileAccessMetricStatus::kOK
: FileAccessMetricStatus::kBlockedUser,
msg->event_type, policyDecision);
if (decision == RateLimiter::Decision::kAllowed) {
std::string policyNameCopy = policy->name;
std::string policyVersionCopy = policyVersion;
std::string targetPathCopy = target.path;
@@ -486,15 +689,17 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
}];
}
if (!optionalPolicy.value()->silent && self.fileAccessBlockCallback) {
// Notify users on block decisions
if (ShouldNotifyUserDecision(policyDecision) &&
(!policy->silent || (!policy->silent_tty && msg->process->tty->path.length > 0))) {
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.ruleVersion = StringToNSString(policy->version);
event.ruleName = StringToNSString(policy->name);
event.fileSHA256 = cd.sha256 ?: @"<unknown sha>";
event.filePath = StringToNSString(msg->process->executable->path.data);
@@ -503,15 +708,50 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
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());
event.signingChain = cd.certChain;
self.fileAccessBlockCallback(event);
std::pair<NSString *, NSString *> linkInfo = self->_watchItems->EventDetailLinkInfo(policy);
if (!policy->silent && self.fileAccessBlockCallback) {
self.fileAccessBlockCallback(event, OptionalStringToNSString(policy->custom_message),
linkInfo.first, linkInfo.second);
}
if (ShouldMessageTTY(policy, msg, self->_ttyMessageCache)) {
NSAttributedString *attrStr =
[SNTBlockMessage attributedBlockMessageForFileAccessEvent:event
customMessage:OptionalStringToNSString(
policy->custom_message)];
NSMutableString *blockMsg = [NSMutableString stringWithCapacity:1024];
// Escape sequences `\033[1m` and `\033[0m` begin/end bold lettering
[blockMsg appendFormat:@"\n\033[1mSanta\033[0m\n\n%@\n\n", attrStr.string];
[blockMsg appendFormat:@"\033[1mAccessed Path:\033[0m %@\n"
@"\033[1mRule Version: \033[0m %@\n"
@"\033[1mRule Name: \033[0m %@\n"
@"\n"
@"\033[1mProcess Path: \033[0m %@\n"
@"\033[1mIdentifier: \033[0m %@\n"
@"\033[1mParent: \033[0m %@\n\n",
event.accessedPath, event.ruleVersion, event.ruleName,
event.filePath, event.fileSHA256, event.parentName];
NSURL *detailURL = [SNTBlockMessage eventDetailURLForFileAccessEvent:event
customURL:linkInfo.first];
if (detailURL) {
[blockMsg appendFormat:@"More info:\n%@\n\n", detailURL.absoluteString];
}
self->_ttyWriter->Write(msg->process, blockMsg);
}
}
}
return policyDecision;
}
- (void)processMessage:(const Message &)msg {
- (void)processMessage:(const Message &)msg
overrideAction:(SNTOverrideFileAccessAction)overrideAction {
std::vector<PathTarget> targets;
targets.reserve(2);
PopulatePathTargets(msg, targets);
@@ -533,7 +773,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
FileAccessPolicyDecision curDecision = [self handleMessage:msg
target:targets[i]
policy:versionAndPolicies.second[i]
policyVersion:versionAndPolicies.first];
policyVersion:versionAndPolicies.first
overrideAction:overrideAction];
policyResult =
CombinePolicyResults(policyResult, FileAccessPolicyDecisionToESAuthResult(curDecision));
@@ -558,19 +799,44 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
- (void)handleMessage:(santa::santad::event_providers::endpoint_security::Message &&)esMsg
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
SNTOverrideFileAccessAction overrideAction = [self.configurator overrideFileAccessAction];
// If the override action is set to Disable, return immediately.
if (overrideAction == SNTOverrideFileAccessActionDiable) {
if (esMsg->action_type == ES_ACTION_TYPE_AUTH) {
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:false];
}
return;
}
if (esMsg->event_type == ES_EVENT_TYPE_AUTH_OPEN &&
!(esMsg->event.open.fflag & kOpenFlagsIndicatingWrite)) {
if (self->_readsCache.Exists(esMsg->process, ^std::pair<pid_t, pid_t> {
return std::pair<dev_t, ino_t>{esMsg->event.open.file->stat.st_dev,
esMsg->event.open.file->stat.st_ino};
})) {
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:false];
return;
}
} else if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_EXIT) {
// On process exit, remove the cache entry
self->_readsCache.Remove(esMsg->process);
self->_ttyMessageCache.Remove(esMsg->process);
return;
}
[self processMessage:std::move(esMsg)
handler:^(const Message &msg) {
[self processMessage:msg];
[self processMessage:msg overrideAction:overrideAction];
recordEventMetrics(EventDisposition::kProcessed);
}];
}
- (void)enable {
// TODO(xyz): Expand to support ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_TRUNCATE
std::set<es_event_type_t> events = {
ES_EVENT_TYPE_AUTH_CLONE, ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_EXCHANGEDATA,
ES_EVENT_TYPE_AUTH_LINK, ES_EVENT_TYPE_AUTH_OPEN, ES_EVENT_TYPE_AUTH_RENAME,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK, ES_EVENT_TYPE_NOTIFY_EXIT,
};
#if HAVE_MACOS_12
@@ -614,6 +880,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
// begin receiving events (if not already)
[self enable];
}
self->_readsCache.Clear();
}
@end

View File

@@ -20,7 +20,9 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <sys/fcntl.h>
#include <sys/types.h>
#include <cstring>
#include <utility>
#include <array>
#include <cstddef>
@@ -31,6 +33,7 @@
#include "Source/common/Platform.h"
#include "Source/common/SNTCachedDecision.h"
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"
@@ -50,13 +53,22 @@ extern NSString *kBadCertHash;
struct PathTarget {
std::string path;
bool isReadable;
std::optional<std::pair<dev_t, ino_t>> devnoIno;
};
using PathTargetsPair = std::pair<std::optional<std::string>, std::optional<std::string>>;
extern void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets);
extern es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyDecision decision);
extern bool ShouldLogDecision(FileAccessPolicyDecision decision);
extern bool ShouldNotifyUserDecision(FileAccessPolicyDecision decision);
extern es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_result_t result2);
extern bool IsBlockDecision(FileAccessPolicyDecision decision);
extern FileAccessPolicyDecision ApplyOverrideToDecision(FileAccessPolicyDecision decision,
SNTOverrideFileAccessAction overrideAction);
static inline std::pair<dev_t, ino_t> FileID(const es_file_t &file) {
return std::make_pair(file.stat.st_dev, file.stat.st_ino);
}
void SetExpectationsForFileAccessAuthorizerInit(
std::shared_ptr<MockEndpointSecurityAPI> mockESApi) {
@@ -135,7 +147,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:self.dcMock];
decisionCache:self.dcMock
ttyWriter:nullptr];
//
// Test 1 - Not in local cache or decision cache, and code sig lookup fails
@@ -229,7 +242,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
{FileAccessPolicyDecision::kAllowed, false},
{FileAccessPolicyDecision::kAllowedReadAccess, false},
{FileAccessPolicyDecision::kAllowedAuditOnly, true},
{(FileAccessPolicyDecision)5, false},
{(FileAccessPolicyDecision)123, false},
};
for (const auto &kv : policyDecisionToShouldLog) {
@@ -237,6 +250,79 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
}
}
- (void)testShouldNotifyUserDecision {
std::map<FileAccessPolicyDecision, bool> policyDecisionToShouldLog = {
{FileAccessPolicyDecision::kNoPolicy, false},
{FileAccessPolicyDecision::kDenied, true},
{FileAccessPolicyDecision::kDeniedInvalidSignature, true},
{FileAccessPolicyDecision::kAllowed, false},
{FileAccessPolicyDecision::kAllowedReadAccess, false},
{FileAccessPolicyDecision::kAllowedAuditOnly, false},
{(FileAccessPolicyDecision)123, false},
};
for (const auto &kv : policyDecisionToShouldLog) {
XCTAssertEqual(ShouldNotifyUserDecision(kv.first), kv.second);
}
}
- (void)testIsBlockDecision {
std::map<FileAccessPolicyDecision, bool> policyDecisionToIsBlockDecision = {
{FileAccessPolicyDecision::kNoPolicy, false},
{FileAccessPolicyDecision::kDenied, true},
{FileAccessPolicyDecision::kDeniedInvalidSignature, true},
{FileAccessPolicyDecision::kAllowed, false},
{FileAccessPolicyDecision::kAllowedReadAccess, false},
{FileAccessPolicyDecision::kAllowedAuditOnly, false},
{(FileAccessPolicyDecision)123, false},
};
for (const auto &kv : policyDecisionToIsBlockDecision) {
XCTAssertEqual(ShouldNotifyUserDecision(kv.first), kv.second);
}
}
- (void)testApplyOverrideToDecision {
std::map<std::pair<FileAccessPolicyDecision, SNTOverrideFileAccessAction>,
FileAccessPolicyDecision>
decisionAndOverrideToDecision = {
// Override action: None - Policy shouldn't be changed
{{FileAccessPolicyDecision::kNoPolicy, SNTOverrideFileAccessActionNone},
FileAccessPolicyDecision::kNoPolicy},
{{FileAccessPolicyDecision::kDenied, SNTOverrideFileAccessActionNone},
FileAccessPolicyDecision::kDenied},
// Override action: AuditOnly - Policy should be changed only on blocked decisions
{{FileAccessPolicyDecision::kNoPolicy, SNTOverrideFileAccessActionAuditOnly},
FileAccessPolicyDecision::kNoPolicy},
{{FileAccessPolicyDecision::kAllowedAuditOnly, SNTOverrideFileAccessActionAuditOnly},
FileAccessPolicyDecision::kAllowedAuditOnly},
{{FileAccessPolicyDecision::kAllowedReadAccess, SNTOverrideFileAccessActionAuditOnly},
FileAccessPolicyDecision::kAllowedReadAccess},
{{FileAccessPolicyDecision::kDenied, SNTOverrideFileAccessActionAuditOnly},
FileAccessPolicyDecision::kAllowedAuditOnly},
{{FileAccessPolicyDecision::kDeniedInvalidSignature, SNTOverrideFileAccessActionAuditOnly},
FileAccessPolicyDecision::kAllowedAuditOnly},
// Override action: Disable - Always changes the decision to be no policy applied
{{FileAccessPolicyDecision::kAllowed, SNTOverrideFileAccessActionDiable},
FileAccessPolicyDecision::kNoPolicy},
{{FileAccessPolicyDecision::kDenied, SNTOverrideFileAccessActionDiable},
FileAccessPolicyDecision::kNoPolicy},
{{FileAccessPolicyDecision::kAllowedReadAccess, SNTOverrideFileAccessActionDiable},
FileAccessPolicyDecision::kNoPolicy},
{{FileAccessPolicyDecision::kAllowedAuditOnly, SNTOverrideFileAccessActionDiable},
FileAccessPolicyDecision::kNoPolicy},
};
for (const auto &kv : decisionAndOverrideToDecision) {
XCTAssertEqual(ApplyOverrideToDecision(kv.first.first, kv.first.second), kv.second);
}
XCTAssertThrows(
ApplyOverrideToDecision(FileAccessPolicyDecision::kAllowed, (SNTOverrideFileAccessAction)123));
}
- (void)testCombinePolicyResults {
// Ensure that the combined result is ES_AUTH_RESULT_DENY if both or either
// input result is ES_AUTH_RESULT_DENY.
@@ -269,7 +355,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
auto policy = std::make_shared<WatchItemPolicy>("foo_policy", "/foo");
@@ -397,7 +484,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
id accessClientMock = OCMPartialMock(accessClient);
@@ -515,7 +603,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
id accessClientMock = OCMPartialMock(accessClient);
@@ -642,7 +731,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
std::set<es_event_type_t> expectedEventSubs = {
ES_EVENT_TYPE_AUTH_CLONE, ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_EXCHANGEDATA,
ES_EVENT_TYPE_AUTH_LINK, ES_EVENT_TYPE_AUTH_OPEN, ES_EVENT_TYPE_AUTH_RENAME,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK, ES_EVENT_TYPE_NOTIFY_EXIT,
};
#if HAVE_MACOS_12
@@ -678,7 +767,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
EXPECT_CALL(*mockESApi, UnsubscribeAll);
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
@@ -689,12 +779,12 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testGetPathTargets {
- (void)testPopulatePathTargets {
// This test ensures that the `GetPathTargets` functions returns the
// expected combination of targets for each handled event variant
es_file_t testFile1 = MakeESFile("test_file_1");
es_file_t testFile2 = MakeESFile("test_file_2");
es_file_t testDir = MakeESFile("test_dir");
es_file_t testFile1 = MakeESFile("test_file_1", MakeStat(100));
es_file_t testFile2 = MakeESFile("test_file_2", MakeStat(200));
es_file_t testDir = MakeESFile("test_dir", MakeStat(300));
es_string_token_t testTok = MakeESStringToken("test_tok");
std::string dirTok = std::string(testDir.path.data) + "/" + std::string(testTok.data);
@@ -715,6 +805,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
}
{
@@ -729,8 +820,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -747,8 +840,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -762,8 +857,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
}
@@ -777,6 +874,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
}
{
@@ -791,8 +889,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -806,8 +906,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -822,6 +924,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCppStringEqual(targets[0].path, dirTok);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
}
{
@@ -834,6 +937,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
}
if (@available(macOS 12.0, *)) {
@@ -852,8 +956,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -865,8 +971,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
}
}

View File

@@ -88,9 +88,23 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
// Pre-enrichment processing
switch (esMsg->event_type) {
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) {
BOOL shouldLogClose = esMsg->event.close.modified;
#if HAVE_MACOS_13
if (@available(macOS 13.5, *)) {
// As of macSO 13.0 we have a new field for if a file was mmaped with
// write permissions on close events. However it did not work until
// 13.5.
//
// If something was mmaped writable it was probably written to. Often
// developer tools do this to avoid lots of write syscalls, e.g. go's
// tool chain. We log this so the compiler controller can take that into
// account.
shouldLogClose |= esMsg->event.close.was_mapped_writable;
}
#endif
if (!shouldLogClose) {
// Ignore unmodified files
// Note: Do not record metrics in this case. These are not considered "drops"
// because this is not a failure case. Ideally we would tell ES to not send
@@ -115,7 +129,6 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
break;
}
default: break;
}

View File

@@ -17,6 +17,7 @@
#import <XCTest/XCTest.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <objc/NSObjCRuntime.h>
#include <cstddef>
#include <memory>
@@ -102,12 +103,21 @@ class MockLogger : public Logger {
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testHandleMessage {
typedef void (^testHelperBlock)(es_message_t *message,
std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient,
std::shared_ptr<PrefixTree<Unit>> prefixTree,
dispatch_semaphore_t *sema, dispatch_semaphore_t *semaMetrics);
es_file_t targetFileMatchesRegex = MakeESFile("/foo/matches");
es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
- (void)handleMessageWithMatchCalls:(BOOL)regexMatchCalls
withMissCalls:(BOOL)regexFailsMatchCalls
withBlock:(testHelperBlock)testBlock {
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 targetFileMatchesRegex = MakeESFile("/foo/matches");
es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
@@ -116,11 +126,15 @@ class MockLogger : public Logger {
std::unique_ptr<EnrichedMessage> enrichedMsg = std::unique_ptr<EnrichedMessage>(nullptr);
auto mockEnricher = std::make_shared<MockEnricher>();
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
if (regexMatchCalls) {
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
}
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMatchesRegex)).Times(1);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMissesRegex)).Times(1);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMatchesRegex)).Times((int)regexMatchCalls);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMissesRegex))
.Times((int)regexFailsMatchCalls);
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
@@ -128,11 +142,13 @@ class MockLogger : public Logger {
// `SNTEndpointSecurityRecorder` object. There is a bug in OCMock that doesn't
// properly handle the `processEnrichedMessage:handler:` block. Instead this
// test will mock the `Log` method that is called in the handler block.
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
auto mockLogger = std::make_shared<MockLogger>(nullptr, nullptr);
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
dispatch_semaphore_signal(sema);
}));
if (regexMatchCalls) {
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
dispatch_semaphore_signal(sema);
}));
}
auto prefixTree = std::make_shared<PrefixTree<Unit>>();
@@ -147,66 +163,7 @@ class MockLogger : public Logger {
authResultCache:mockAuthCache
prefixTree:prefixTree];
// CLOSE not modified, bail early
{
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
esMsg.event.close.modified = false;
esMsg.event.close.target = NULL;
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, &esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTFail("Metrics record callback should not be called here");
}]);
}
// 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 = &targetFileMatchesRegex;
Message msg(mockESApi, &esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
[recorderClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
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 = &targetFileMatchesRegex;
prefixTree->InsertPrefix(esMsg.event.link.source->path.data, Unit{});
Message msg(mockESApi, &esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
[recorderClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kDropped);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
}
testBlock(&esMsg, mockESApi, mockCC, recorderClient, prefixTree, &sema, &semaMetrics);
XCTAssertTrue(OCMVerifyAll(mockCC));
@@ -218,6 +175,150 @@ class MockLogger : public Logger {
[mockCC stopMocking];
}
- (void)testHandleMessageWithCloseMappedWriteable {
#if HAVE_MACOS_13
if (@available(macOS 13.0, *)) {
// CLOSE not modified, but was_mapped_writable, should remove from cache,
// and matches fileChangesRegex
testHelperBlock testBlock =
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema,
__autoreleasing dispatch_semaphore_t *semaMetrics) {
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
esMsg->event.close.modified = false;
esMsg->event.close.was_mapped_writable = true;
esMsg->event.close.target = &targetFileMatchesRegex;
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(*semaMetrics);
}]);
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertSemaTrue(*sema, 5, "Log wasn't called within expected time window");
};
[self handleMessageWithMatchCalls:YES withMissCalls:NO withBlock:testBlock];
}
#endif
}
- (void)testHandleEventCloseNotModifiedWithWasMappedWritable {
#if HAVE_MACOS_13
if (@available(macOS 13.0, *)) {
// CLOSE not modified, but was_mapped_writable, remove from cache, and does not match
// fileChangesRegex
testHelperBlock testBlock =
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema,
__autoreleasing dispatch_semaphore_t *semaMetrics) {
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
esMsg->event.close.modified = false;
esMsg->event.close.was_mapped_writable = 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");
}]);
};
[self handleMessageWithMatchCalls:NO withMissCalls:YES withBlock:testBlock];
}
#endif
}
- (void)testHandleMessage {
// CLOSE not modified, bail early
testHelperBlock testBlock = ^(
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
esMsg->event.close.modified = false;
esMsg->event.close.target = NULL;
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTFail("Metrics record callback should not be called here");
}]);
};
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
// CLOSE modified, remove from cache, and matches fileChangesRegex
testBlock = ^(
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
esMsg->event.close.modified = true;
esMsg->event.close.target = &targetFileMatchesRegex;
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
[recorderClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(*semaMetrics);
}];
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertSemaTrue(*sema, 5, "Log wasn't called within expected time window");
};
[self handleMessageWithMatchCalls:YES withMissCalls:NO withBlock:testBlock];
// CLOSE modified, remove from cache, but doesn't match fileChangesRegex
testBlock = ^(
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
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");
}]);
};
[self handleMessageWithMatchCalls:NO withMissCalls:YES withBlock:testBlock];
// LINK, Prefix match, bail early
testBlock =
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics)
{
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_LINK;
esMsg->event.link.source = &targetFileMatchesRegex;
prefixTree->InsertPrefix(esMsg->event.link.source->path.data, Unit{});
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
[recorderClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kDropped);
dispatch_semaphore_signal(*semaMetrics);
}];
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
};
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
}
- (void)testGetTargetFileForPrefixTree {
// Ensure `GetTargetFileForPrefixTree` returns expected field for each
// subscribed event type in the `SNTEndpointSecurityRecorder`.

View File

@@ -46,6 +46,7 @@ using santa::santad::event_providers::endpoint_security::EnrichedProcess;
using santa::santad::event_providers::endpoint_security::EnrichedRename;
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::serializers::Utilities::MountFromName;
using santa::santad::logs::endpoint_security::serializers::Utilities::NonNull;
using santa::santad::logs::endpoint_security::serializers::Utilities::Pid;
using santa::santad::logs::endpoint_security::serializers::Utilities::Pidversion;
@@ -509,6 +510,8 @@ std::vector<uint8_t> BasicString::SerializeDiskAppeared(NSDictionary *props) {
str.append([NonNull(dmg_path) UTF8String]);
str.append("|appearance=");
str.append([NonNull(appearanceDateString) UTF8String]);
str.append("|mountfrom=");
str.append([NonNull(MountFromName([props[@"DAVolumePath"] path])) UTF8String]);
return FinalizeString(str);
}

View File

@@ -351,7 +351,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
@"DADeviceVendor" : @"vendor",
@"DADeviceModel" : @"model",
@"DAAppearanceTime" : @(1252487349), // 2009-09-09 09:09:09
@"DAVolumePath" : [NSURL URLWithString:@"path"],
@"DAVolumePath" : [NSURL URLWithString:@"/"],
@"DAMediaBSDName" : @"bsd",
@"DAVolumeKind" : @"apfs",
@"DADeviceProtocol" : @"usb",
@@ -365,11 +365,11 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
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"
std::string want = "action=DISKAPPEAR|mount=/|volume=|bsdname=bsd|fs=apfs"
"|model=vendor model|serial=|bus=usb|dmgpath="
"|appearance=2040-09-09T09:09:09.000Z\n";
"|appearance=2040-09-09T09:09:09.000Z|mountfrom=/";
XCTAssertCppStringEqual(got, want);
XCTAssertCppStringBeginsWith(got, want);
}
- (void)testSerializeDiskDisappeared {

View File

@@ -17,8 +17,7 @@
#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 <google/protobuf/json/json.h>
#include <mach/message.h>
#include <math.h>
#include <sys/proc_info.h>
@@ -36,12 +35,13 @@
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
#import "Source/santad/SNTDecisionCache.h"
#include "absl/status/status.h"
#include "google/protobuf/timestamp.pb.h"
using google::protobuf::Arena;
using google::protobuf::Timestamp;
using google::protobuf::util::JsonPrintOptions;
using google::protobuf::util::MessageToJsonString;
using JsonPrintOptions = google::protobuf::json::PrintOptions;
using google::protobuf::json::MessageToJsonString;
using santa::common::NSStringToUTF8StringView;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
@@ -59,6 +59,7 @@ using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveGroup;
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveUser;
using santa::santad::logs::endpoint_security::serializers::Utilities::MountFromName;
using santa::santad::logs::endpoint_security::serializers::Utilities::NonNull;
using santa::santad::logs::endpoint_security::serializers::Utilities::Pid;
using santa::santad::logs::endpoint_security::serializers::Utilities::Pidversion;
@@ -400,7 +401,7 @@ std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
options.preserve_proto_field_names = true;
std::string json;
google::protobuf::util::Status status = MessageToJsonString(*santa_msg, &json, options);
absl::Status status = MessageToJsonString(*santa_msg, &json, options);
if (!status.ok()) {
LOGE(@"Failed to convert protobuf to JSON: %s", status.ToString().c_str());
@@ -682,6 +683,8 @@ static void EncodeDisk(::pbv1::Disk *pb_disk, ::pbv1::Disk_Action action, NSDict
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);
EncodeString([pb_disk] { return pb_disk->mutable_mount_from(); },
MountFromName([props[@"DAVolumePath"] path]));
if (props[@"DAAppearanceTime"]) {
// Note: `DAAppearanceTime` is set via `CFAbsoluteTimeGetCurrent`, which uses the defined

View File

@@ -18,7 +18,7 @@
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <gmock/gmock.h>
#include <google/protobuf/util/json_util.h>
#include <google/protobuf/json/json.h>
#include <gtest/gtest.h>
#include <sys/proc_info.h>
#include <sys/signal.h>
@@ -40,12 +40,15 @@
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
#import "Source/santad/SNTDecisionCache.h"
#include "absl/status/status.h"
#include "google/protobuf/any.pb.h"
#include "google/protobuf/timestamp.pb.h"
using google::protobuf::Timestamp;
using google::protobuf::util::JsonPrintOptions;
using google::protobuf::util::JsonStringToMessage;
using JsonPrintOptions = google::protobuf::json::PrintOptions;
using JsonParseOptions = ::google::protobuf::json::ParseOptions;
using google::protobuf::json::JsonStringToMessage;
using google::protobuf::json::MessageToJsonString;
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
using santa::santad::event_providers::endpoint_security::Enricher;
@@ -157,7 +160,7 @@ std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
const google::protobuf::Message &message = SantaMessageEvent(santaMsg);
std::string json;
XCTAssertTrue(google::protobuf::util::MessageToJsonString(message, &json, options).ok());
XCTAssertTrue(MessageToJsonString(message, &json, options).ok());
return json;
}
@@ -236,9 +239,10 @@ void SerializeAndCheck(es_event_type_t eventType,
if (json) {
// Parse the jsonified string into the protobuf
// gotData = protoStr;
google::protobuf::util::JsonParseOptions options;
JsonParseOptions options;
options.ignore_unknown_fields = true;
google::protobuf::util::Status status = JsonStringToMessage(protoStr, &santaMsg, options);
absl::Status status = JsonStringToMessage(protoStr, &santaMsg, options);
XCTAssertTrue(status.ok());
gotData = ConvertMessageToJsonString(santaMsg);
} else {
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
@@ -767,7 +771,7 @@ void SerializeAndCheckNonESEvents(
@"DADeviceVendor" : @"vendor",
@"DADeviceModel" : @"model",
@"DAAppearanceTime" : @(123456789),
@"DAVolumePath" : [NSURL URLWithString:@"path"],
@"DAVolumePath" : [NSURL URLWithString:@"/"],
@"DAMediaBSDName" : @"bsd",
@"DAVolumeKind" : @"apfs",
@"DADeviceProtocol" : @"usb",
@@ -792,6 +796,7 @@ void SerializeAndCheckNonESEvents(
XCTAssertEqualObjects(@(pbDisk.serial().c_str()), @"");
XCTAssertEqualObjects(@(pbDisk.bus().c_str()), props[@"DADeviceProtocol"]);
XCTAssertEqualObjects(@(pbDisk.dmg_path().c_str()), @"");
XCTAssertCppStringBeginsWith(pbDisk.mount_from(), std::string("/"));
// Note: `DAAppearanceTime` is treated as a reference time since 2001 and is converted to a
// reference time of 1970. Skip the calculation in the test here, just ensure the value is set.

View File

@@ -54,6 +54,8 @@ static inline NSString *NonNull(NSString *str) {
NSString *OriginalPathForTranslocation(const es_process_t *es_proc);
NSString *SerialForDevice(NSString *devPath);
NSString *DiskImageForDevice(NSString *devPath);
NSString *MountFromName(NSString *path);
es_file_t *GetAllowListTargetFile(
const santa::santad::event_providers::endpoint_security::Message &msg);

View File

@@ -14,9 +14,13 @@
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
#include <sys/mount.h>
#include <sys/param.h>
#include "Source/common/SantaCache.h"
#import "Source/common/SantaVnode.h"
#include "Source/common/SantaVnodeHash.h"
#include "Source/common/String.h"
// These functions are exported by the Security framework, but are not included in headers
extern "C" Boolean SecTranslocateIsTranslocatedURL(CFURLRef path, bool *isTranslocated,
@@ -114,6 +118,22 @@ NSString *SerialForDevice(NSString *devPath) {
return [serial stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}
NSString *MountFromName(NSString *path) {
if (!path.length) {
return nil;
}
struct statfs sfs;
if (statfs(path.UTF8String, &sfs) != 0) {
return nil;
}
NSString *mntFromName = santa::common::StringToNSString(sfs.f_mntfromname);
return mntFromName.length > 0 ? mntFromName : nil;
}
NSString *DiskImageForDevice(NSString *devPath) {
devPath = [devPath stringByDeletingLastPathComponent];
if (!devPath.length) {

View File

@@ -24,6 +24,7 @@
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::serializers::Utilities::GetAllowListTargetFile;
using santa::santad::logs::endpoint_security::serializers::Utilities::MountFromName;
@interface UtilitiesTest : XCTestCase
@end
@@ -62,4 +63,12 @@ using santa::santad::logs::endpoint_security::serializers::Utilities::GetAllowLi
}
}
- (void)testMountFromName {
XCTAssertNil(MountFromName(@""));
XCTAssertNil(MountFromName(nil));
XCTAssertNil(MountFromName(@"./this/path/should/not/ever/exist/"));
XCTAssertCppStringBeginsWith(std::string(MountFromName(@"/").UTF8String), std::string("/"));
}
@end

View File

@@ -10,6 +10,7 @@ proto_library(
srcs = ["binaryproto.proto"],
deps = [
"@com_google_protobuf//:any_proto",
"@com_google_protobuf//:timestamp_proto",
],
)
@@ -69,6 +70,7 @@ santa_unit_test(
deps = [
":fsspool",
":fsspool_log_batch_writer",
"//Source/common:TestUtils",
"@OCMock",
],
)

View File

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

View File

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

View File

@@ -33,7 +33,6 @@ FsSpoolLogBatchWriter::~FsSpoolLogBatchWriter() {
if (!s.ok()) {
os_log(OS_LOG_DEFAULT, "Flush() failed with %s",
s.ToString(absl::StatusToStringMode::kWithEverything).c_str());
// LOG(WARNING) << "Flush() failed with " << s;
}
}

View File

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

View File

@@ -22,7 +22,9 @@
#include <map>
#include <memory>
#include <string>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTMetricSet.h"
namespace santa::santad {
@@ -44,8 +46,18 @@ enum class Processor {
kFileAccessAuthorizer,
};
enum class FileAccessMetricStatus {
kOK = 0,
kBlockedUser,
};
using EventCountTuple = std::tuple<Processor, es_event_type_t, EventDisposition>;
using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
using FileAccessMetricsPolicyVersion = std::string;
using FileAccessMetricsPolicyName = std::string;
using FileAccessEventCountTuple =
std::tuple<FileAccessMetricsPolicyVersion, FileAccessMetricsPolicyName, FileAccessMetricStatus,
es_event_type_t, FileAccessPolicyDecision>;
class Metrics : public std::enable_shared_from_this<Metrics> {
public:
@@ -53,8 +65,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
SNTMetricCounter *rate_limit_counts, SNTMetricSet *metric_set,
void (^run_on_first_start)(Metrics *));
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *));
~Metrics();
@@ -71,6 +83,10 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
void SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count);
void SetFileAccessEventMetrics(std::string policy_version, std::string rule_name,
FileAccessMetricStatus status, es_event_type_t event_type,
FileAccessPolicyDecision decision);
friend class santa::santad::MetricsPeer;
private:
@@ -84,6 +100,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
SNTMetricInt64Gauge *event_processing_times_;
SNTMetricCounter *event_counts_;
SNTMetricCounter *rate_limit_counts_;
SNTMetricCounter *faa_event_counts_;
SNTMetricSet *metric_set_;
// Tracks whether or not the timer_source should be running.
// This helps manage dispatch source state to ensure the source is not
@@ -99,6 +116,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
std::map<EventCountTuple, int64_t> event_counts_cache_;
std::map<EventTimesTuple, int64_t> event_times_cache_;
std::map<Processor, int64_t> rate_limit_counts_cache_;
std::map<FileAccessEventCountTuple, int64_t> faa_event_counts_cache_;
};
} // namespace santa::santad

View File

@@ -53,6 +53,16 @@ static NSString *const kEventTypeNotifyUnmount = @"NotifyUnmount";
static NSString *const kEventDispositionDropped = @"Dropped";
static NSString *const kEventDispositionProcessed = @"Processed";
// Compat values
static NSString *const kFileAccessMetricStatusOK = @"OK";
static NSString *const kFileAccessMetricStatusBlockedUser = @"BLOCKED_USER";
static NSString *const kFileAccessPolicyDecisionDenied = @"Denied";
static NSString *const kFileAccessPolicyDecisionDeniedInvalidSignature = @"Denied";
static NSString *const kFileAccessPolicyDecisionAllowedAuditOnly = @"AllowedAuditOnly";
static NSString *const kFileAccessMetricsAccessType = @"access";
namespace santa::santad {
NSString *const ProcessorToString(Processor processor) {
@@ -63,7 +73,8 @@ NSString *const ProcessorToString(Processor processor) {
case Processor::kTamperResistance: return kProcessorTamperResistance;
case Processor::kFileAccessAuthorizer: return kProcessorFileAccessAuthorizer;
default:
[NSException raise:@"Invalid processor" format:@"Unknown processor value: %d", processor];
[NSException raise:@"Invalid processor"
format:@"Unknown processor value: %d", static_cast<int>(processor)];
return nil;
}
}
@@ -103,7 +114,34 @@ NSString *const EventDispositionToString(EventDisposition d) {
case EventDisposition::kDropped: return kEventDispositionDropped;
case EventDisposition::kProcessed: return kEventDispositionProcessed;
default:
[NSException raise:@"Invalid disposition" format:@"Unknown disposition value: %d", d];
[NSException raise:@"Invalid disposition"
format:@"Unknown disposition value: %d", static_cast<int>(d)];
return nil;
}
}
NSString *const FileAccessMetricStatusToString(FileAccessMetricStatus status) {
switch (status) {
case FileAccessMetricStatus::kOK: return kFileAccessMetricStatusOK;
case FileAccessMetricStatus::kBlockedUser: return kFileAccessMetricStatusBlockedUser;
default:
[NSException raise:@"Invalid file access metric status"
format:@"Unknown file access metric status value: %d", static_cast<int>(status)];
return nil;
}
}
NSString *const FileAccessPolicyDecisionToString(FileAccessPolicyDecision decision) {
switch (decision) {
case FileAccessPolicyDecision::kDenied: return kFileAccessPolicyDecisionDenied;
case FileAccessPolicyDecision::kDeniedInvalidSignature:
return kFileAccessPolicyDecisionDeniedInvalidSignature;
case FileAccessPolicyDecision::kAllowedAuditOnly:
return kFileAccessPolicyDecisionAllowedAuditOnly;
default:
[NSException
raise:@"Invalid file access policy decision"
format:@"Unknown file access policy decision value: %d", static_cast<int>(decision)];
return nil;
}
}
@@ -129,9 +167,16 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
fieldNames:@[ @"Processor" ]
helpText:@"Events rate limited by each processor"];
SNTMetricCounter *faa_event_counts = [[SNTMetricSet sharedInstance]
counterWithName:@"/santa/file_access_authorizer/log/count"
fieldNames:@[
@"config_version", @"access_type", @"rule_id", @"status", @"operation", @"decision"
]
helpText:@"Count of times a log is emitted from the File Access Authorizer client"];
std::shared_ptr<Metrics> metrics =
std::make_shared<Metrics>(q, timer_source, interval, event_processing_times, event_counts,
rate_limit_counts, metric_set, ^(Metrics *metrics) {
rate_limit_counts, faa_event_counts, metric_set, ^(Metrics *metrics) {
SNTRegisterCoreMetrics();
metrics->EstablishConnection();
});
@@ -151,14 +196,15 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
SNTMetricCounter *rate_limit_counts, SNTMetricSet *metric_set,
void (^run_on_first_start)(Metrics *))
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *))
: q_(q),
timer_source_(timer_source),
interval_(interval),
event_processing_times_(event_processing_times),
event_counts_(event_counts),
rate_limit_counts_(rate_limit_counts),
faa_event_counts_(faa_event_counts),
metric_set_(metric_set),
run_on_first_start_(run_on_first_start) {
SetInterval(interval_);
@@ -224,10 +270,26 @@ void Metrics::FlushMetrics() {
[rate_limit_counts_ incrementBy:kv.second forFieldValues:@[ processorName ]];
}
for (const auto &kv : faa_event_counts_cache_) {
NSString *policyVersion = @(std::get<0>(kv.first).c_str()); // FileAccessMetricsPolicyVersion
NSString *policyName = @(std::get<1>(kv.first).c_str()); // FileAccessMetricsPolicyName
NSString *eventName = EventTypeToString(std::get<es_event_type_t>(kv.first));
NSString *status = FileAccessMetricStatusToString(std::get<FileAccessMetricStatus>(kv.first));
NSString *decision =
FileAccessPolicyDecisionToString(std::get<FileAccessPolicyDecision>(kv.first));
[faa_event_counts_
incrementBy:kv.second
forFieldValues:@[
policyVersion, kFileAccessMetricsAccessType, policyName, status, eventName, decision
]];
}
// Reset the maps so the next cycle begins with a clean state
event_counts_cache_ = {};
event_times_cache_ = {};
rate_limit_counts_cache_ = {};
faa_event_counts_cache_ = {};
});
}
@@ -283,4 +345,13 @@ void Metrics::SetRateLimitingMetrics(Processor processor, int64_t events_rate_li
});
}
void Metrics::SetFileAccessEventMetrics(std::string policy_version, std::string rule_name,
FileAccessMetricStatus status, es_event_type_t event_type,
FileAccessPolicyDecision decision) {
dispatch_sync(events_q_, ^{
faa_event_counts_cache_[FileAccessEventCountTuple{
std::move(policy_version), std::move(rule_name), status, event_type, decision}]++;
});
}
} // namespace santa::santad

View File

@@ -24,37 +24,44 @@
#include "Source/common/TestUtils.h"
#include "Source/santad/Metrics.h"
using santa::santad::EventCountTuple;
using santa::santad::EventDisposition;
using santa::santad::EventTimesTuple;
using santa::santad::FileAccessEventCountTuple;
using santa::santad::Processor;
using EventCountTuple = std::tuple<Processor, es_event_type_t, EventDisposition>;
using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
namespace santa::santad {
extern NSString *const ProcessorToString(Processor processor);
extern NSString *const EventTypeToString(es_event_type_t eventType);
extern NSString *const EventDispositionToString(EventDisposition d);
extern NSString *const FileAccessMetricStatusToString(FileAccessMetricStatus status);
extern NSString *const FileAccessPolicyDecisionToString(FileAccessPolicyDecision decision);
class MetricsPeer : public Metrics {
public:
// Make base class constructors visible
using Metrics::FlushMetrics;
using Metrics::Metrics;
bool IsRunning() { return running_; }
// Private methods
using Metrics::FlushMetrics;
uint64_t Interval() { return interval_; }
std::map<EventCountTuple, int64_t> &EventCounts() { return event_counts_cache_; };
std::map<EventTimesTuple, int64_t> &EventTimes() { return event_times_cache_; };
std::map<Processor, int64_t> &RateLimitCounts() { return rate_limit_counts_cache_; };
// Private member variables
using Metrics::event_counts_cache_;
using Metrics::event_times_cache_;
using Metrics::faa_event_counts_cache_;
using Metrics::interval_;
using Metrics::rate_limit_counts_cache_;
using Metrics::running_;
};
} // namespace santa::santad
using santa::santad::EventDispositionToString;
using santa::santad::EventTypeToString;
using santa::santad::FileAccessMetricStatus;
using santa::santad::FileAccessMetricStatusToString;
using santa::santad::FileAccessPolicyDecisionToString;
using santa::santad::MetricsPeer;
using santa::santad::ProcessorToString;
@@ -73,19 +80,19 @@ using santa::santad::ProcessorToString;
- (void)testStartStop {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m) {
dispatch_semaphore_signal(self.sema);
});
XCTAssertFalse(metrics->IsRunning());
XCTAssertFalse(metrics->running_);
metrics->StartPoll();
XCTAssertEqual(0, dispatch_semaphore_wait(self.sema, DISPATCH_TIME_NOW),
"Initialization block never called");
// Should be marked running after starting
XCTAssertTrue(metrics->IsRunning());
XCTAssertTrue(metrics->running_);
metrics->StartPoll();
@@ -94,29 +101,29 @@ using santa::santad::ProcessorToString;
"Initialization block called second time unexpectedly");
// Double-start doesn't change the running state
XCTAssertTrue(metrics->IsRunning());
XCTAssertTrue(metrics->running_);
metrics->StopPoll();
// After stopping, the internal state is no longer marked running
XCTAssertFalse(metrics->IsRunning());
XCTAssertFalse(metrics->running_);
metrics->StopPoll();
// Double-stop doesn't change the running state
XCTAssertFalse(metrics->IsRunning());
XCTAssertFalse(metrics->running_);
}
- (void)testSetInterval {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
});
XCTAssertEqual(100, metrics->Interval());
XCTAssertEqual(100, metrics->interval_);
metrics->SetInterval(200);
XCTAssertEqual(200, metrics->Interval());
XCTAssertEqual(200, metrics->interval_);
}
- (void)testProcessorToString {
@@ -179,25 +186,60 @@ using santa::santad::ProcessorToString;
XCTAssertThrows(EventDispositionToString((EventDisposition)12345));
}
- (void)testFileAccessMetricStatusToString {
std::map<FileAccessMetricStatus, NSString *> statusToString = {
{FileAccessMetricStatus::kOK, @"OK"},
{FileAccessMetricStatus::kBlockedUser, @"BLOCKED_USER"},
};
for (const auto &kv : statusToString) {
XCTAssertEqualObjects(FileAccessMetricStatusToString(kv.first), kv.second);
}
XCTAssertThrows(FileAccessMetricStatusToString((FileAccessMetricStatus)12345));
}
- (void)testFileAccessPolicyDecisionToString {
std::map<FileAccessPolicyDecision, NSString *> decisionToString = {
{FileAccessPolicyDecision::kDenied, @"Denied"},
{FileAccessPolicyDecision::kDeniedInvalidSignature, @"Denied"},
{FileAccessPolicyDecision::kDeniedInvalidSignature, @"Denied"},
};
for (const auto &kv : decisionToString) {
XCTAssertEqualObjects(FileAccessPolicyDecisionToString(kv.first), kv.second);
}
std::set<FileAccessPolicyDecision> decisionToStringThrows = {
FileAccessPolicyDecision::kNoPolicy,
FileAccessPolicyDecision::kAllowed,
FileAccessPolicyDecision::kAllowedReadAccess,
(FileAccessPolicyDecision)12345,
};
for (const auto &v : decisionToStringThrows) {
XCTAssertThrows(FileAccessPolicyDecisionToString(v));
}
}
- (void)testSetEventMetrics {
int64_t nanos = 1234;
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
// Initial maps are empty
XCTAssertEqual(metrics->EventCounts().size(), 0);
XCTAssertEqual(metrics->EventTimes().size(), 0);
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
EventDisposition::kProcessed, nanos);
// Check sizes after setting metrics once
XCTAssertEqual(metrics->EventCounts().size(), 1);
XCTAssertEqual(metrics->EventTimes().size(), 1);
XCTAssertEqual(metrics->event_counts_cache_.size(), 1);
XCTAssertEqual(metrics->event_times_cache_.size(), 1);
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
EventDisposition::kProcessed, nanos);
@@ -205,8 +247,8 @@ using santa::santad::ProcessorToString;
EventDisposition::kProcessed, nanos * 2);
// Re-check expected counts. One was an update, so should only be 2 items
XCTAssertEqual(metrics->EventCounts().size(), 2);
XCTAssertEqual(metrics->EventTimes().size(), 2);
XCTAssertEqual(metrics->event_counts_cache_.size(), 2);
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
// Check map values
EventCountTuple ecExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
@@ -216,36 +258,72 @@ using santa::santad::ProcessorToString;
EventTimesTuple etExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC};
EventTimesTuple etOpen{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN};
XCTAssertEqual(metrics->EventCounts()[ecExec], 2);
XCTAssertEqual(metrics->EventCounts()[ecOpen], 1);
XCTAssertEqual(metrics->EventTimes()[etExec], nanos);
XCTAssertEqual(metrics->EventTimes()[etOpen], nanos * 2);
XCTAssertEqual(metrics->event_counts_cache_[ecExec], 2);
XCTAssertEqual(metrics->event_counts_cache_[ecOpen], 1);
XCTAssertEqual(metrics->event_times_cache_[etExec], nanos);
XCTAssertEqual(metrics->event_times_cache_[etOpen], nanos * 2);
}
- (void)testSetRateLimitingMetrics {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
// Initial map is empty
XCTAssertEqual(metrics->RateLimitCounts().size(), 0);
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
// Check sizes after setting metrics once
XCTAssertEqual(metrics->RateLimitCounts().size(), 1);
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 1);
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 456);
metrics->SetRateLimitingMetrics(Processor::kAuthorizer, 789);
// Re-check expected counts. One was an update, so should only be 2 items
XCTAssertEqual(metrics->RateLimitCounts().size(), 2);
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 2);
// Check map values
XCTAssertEqual(metrics->RateLimitCounts()[Processor::kFileAccessAuthorizer], 123 + 456);
XCTAssertEqual(metrics->RateLimitCounts()[Processor::kAuthorizer], 789);
XCTAssertEqual(metrics->rate_limit_counts_cache_[Processor::kFileAccessAuthorizer], 123 + 456);
XCTAssertEqual(metrics->rate_limit_counts_cache_[Processor::kAuthorizer], 789);
}
- (void)testSetFileAccessEventMetrics {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
// Initial map is empty
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
// Check sizes after setting metrics once
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 1);
// Update the previous metric
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
// Add a second metric
metrics->SetFileAccessEventMetrics("v1.0", "rule_xyz", FileAccessMetricStatus::kOK,
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
// Re-check expected counts. One was an update, so should only be 2 items
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 2);
FileAccessEventCountTuple ruleAbc{"v1.0", "rule_abc", FileAccessMetricStatus::kOK,
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied};
FileAccessEventCountTuple ruleXyz{"v1.0", "rule_xyz", FileAccessMetricStatus::kOK,
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied};
XCTAssertEqual(metrics->faa_event_counts_cache_[ruleAbc], 2);
XCTAssertEqual(metrics->faa_event_counts_cache_[ruleXyz], 1);
}
- (void)testFlushMetrics {
@@ -266,22 +344,26 @@ using santa::santad::ProcessorToString;
});
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes,
mockEventCounts, mockEventCounts, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
auto metrics =
std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes, mockEventCounts,
mockEventCounts, mockEventCounts, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
EventDisposition::kProcessed, nanos);
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN,
EventDisposition::kProcessed, nanos * 2);
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
// First ensure we have the expected map sizes
XCTAssertEqual(metrics->EventCounts().size(), 2);
XCTAssertEqual(metrics->EventTimes().size(), 2);
XCTAssertEqual(metrics->RateLimitCounts().size(), 1);
XCTAssertEqual(metrics->event_counts_cache_.size(), 2);
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 1);
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 1);
metrics->FlushMetrics();
@@ -293,11 +375,13 @@ using santa::santad::ProcessorToString;
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (3)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (4)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (5)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (6)");
// After a flush, map sizes should be reset to 0
XCTAssertEqual(metrics->EventCounts().size(), 0);
XCTAssertEqual(metrics->EventTimes().size(), 0);
XCTAssertEqual(metrics->RateLimitCounts().size(), 0);
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
}
@end

View File

@@ -156,6 +156,21 @@ double watchdogRAMPeak = 0;
reply([SNTConfigurator configurator].staticRules.count);
}
- (void)retrieveAllRules:(void (^)(NSArray<SNTRule *> *, NSError *))reply {
SNTConfigurator *config = [SNTConfigurator configurator];
// Do not return any rules if syncBaseURL is set and return an error.
if (config.syncBaseURL) {
reply(@[], [NSError errorWithDomain:@"com.google.santad"
code:403 // (TODO) define error code
userInfo:@{NSLocalizedDescriptionKey : @"SyncBaseURL is set"}]);
return;
}
NSArray<SNTRule *> *rules = [[SNTDatabaseController ruleTable] retrieveAllRules];
reply(rules, nil);
}
#pragma mark Decision Ops
- (void)decisionForFilePath:(NSString *)filePath
@@ -252,6 +267,11 @@ double watchdogRAMPeak = 0;
reply();
}
- (void)setOverrideFileAccessAction:(NSString *)action reply:(void (^)(void))reply {
[[SNTConfigurator configurator] setSyncServerOverrideFileAccessAction:action];
reply();
}
- (void)enableBundles:(void (^)(BOOL))reply {
reply([SNTConfigurator configurator].enableBundles);
}

View File

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

View File

@@ -231,7 +231,7 @@ static NSString *const kPrinterProxyPostMonterey =
// Respond with the decision.
postAction(action);
// Increment counters;
// Increment metric counters
[self incrementEventCounters:cd.decision];
// Log to database if necessary.
@@ -300,27 +300,28 @@ static NSString *const kPrinterProxyPostMonterey =
}
if (!cd.silentBlock) {
// Let the user know what happened, both on the terminal and in the GUI.
NSAttributedString *s = [SNTBlockMessage attributedBlockMessageForEvent:se
customMessage:cd.customMsg];
if (!config.enableSilentTTYMode && self->_ttyWriter && TTYWriter::CanWrite(targetProc)) {
// Let the user know what happened on the terminal
NSAttributedString *s = [SNTBlockMessage attributedBlockMessageForEvent:se
customMessage:cd.customMsg];
if (targetProc->tty && targetProc->tty->path.length > 0 && !config.enableSilentTTYMode &&
self->_ttyWriter) {
NSMutableString *msg = [NSMutableString stringWithCapacity:1024];
// Escape sequences `\033[1m` and `\033[0m` begin/end bold lettering
[msg appendFormat:@"\n\033[1mSanta\033[0m\n\n%@\n\n", s.string];
[msg appendFormat:@"\033[1mPath: \033[0m %@\n"
@"\033[1mIdentifier:\033[0m %@\n"
@"\033[1mParent: \033[0m %@ (%@)\n\n",
se.filePath, se.fileSHA256, se.parentName, se.ppid];
NSURL *detailURL = [SNTBlockMessage eventDetailURLForEvent:se];
NSURL *detailURL = [SNTBlockMessage eventDetailURLForEvent:se customURL:cd.customURL];
if (detailURL) {
[msg appendFormat:@"More info:\n%@\n\n", detailURL.absoluteString];
}
self->_ttyWriter->Write(targetProc->tty->path.data, msg);
self->_ttyWriter->Write(targetProc, msg);
}
[self.notifierQueue addEvent:se customMessage:cd.customMsg];
// Let the user know what happened in the GUI.
[self.notifierQueue addEvent:se withCustomMessage:cd.customMsg andCustomURL:cd.customURL];
}
}
}

View File

@@ -464,6 +464,80 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
[self checkMetricCounters:kAllowTransitive expected:@0];
}
- (void)testSigningIDAllowCompilerRule {
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(YES);
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllowCompiler;
rule.type = SNTRuleTypeSigningID;
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
signingID:signingID
certificateSHA256:nil
teamID:@(kExampleTeamID)])
.andReturn(rule);
[self validateExecEvent:SNTActionRespondAllowCompiler
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kExampleTeamID);
msg->event.exec.target->signing_id = MakeESStringToken(kExampleSigningID);
}];
[self checkMetricCounters:kAllowCompiler expected:@1];
}
- (void)testSigningIDAllowTransitiveRuleDisabled {
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(NO);
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllowTransitive;
rule.type = SNTRuleTypeSigningID;
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
signingID:signingID
certificateSHA256:nil
teamID:nil])
.andReturn(rule);
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
[self validateExecEvent:SNTActionRespondDeny
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->signing_id = MakeESStringToken("com.google.santa.test");
}];
OCMVerifyAllWithDelay(self.mockEventDatabase, 1);
[self checkMetricCounters:kAllowSigningID expected:@0];
[self checkMetricCounters:kAllowTransitive expected:@0];
}
- (void)testThatPlatformBinaryCachedDecisionsSetModeCorrectly {
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(NO);
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
cd.decision = SNTEventStateAllowBinary;
OCMStub([self.mockRuleDatabase criticalSystemBinaries]).andReturn(@{@"a" : cd});
[self validateExecEvent:SNTActionRespondAllow];
[self checkMetricCounters:kAllowBinary expected:@1];
[self checkMetricCounters:kAllowUnknown expected:@0];
XCTAssertEqual(cd.decisionClientMode, SNTClientModeLockdown);
}
- (void)testDefaultDecision {
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");

View File

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

View File

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

View File

@@ -27,6 +27,7 @@
@interface SNTPolicyProcessor ()
@property SNTRuleTable *ruleTable;
@property SNTConfigurator *configurator;
@end
@implementation SNTPolicyProcessor
@@ -35,6 +36,7 @@
self = [super init];
if (self) {
_ruleTable = ruleTable;
_configurator = [SNTConfigurator configurator];
}
return self;
}
@@ -49,10 +51,16 @@
cd.teamID = teamID;
cd.signingID = signingID;
SNTClientMode mode = [self.configurator clientMode];
cd.decisionClientMode = mode;
// 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.
SNTCachedDecision *systemCd = self.ruleTable.criticalSystemBinaries[cd.sha256];
if (systemCd) return systemCd;
if (systemCd) {
systemCd.decisionClientMode = mode;
return systemCd;
}
NSError *csInfoError;
if (certificateSHA256.length) {
@@ -74,27 +82,24 @@
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) {
if (!cd.teamID && cd.signingID) {
id platformID = [csInfo.signingInformation
objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
if (![platformID isKindOfClass:[NSNumber class]] || [platformID intValue] == 0) {
signingID = nil;
cd.signingID = nil;
}
}
cd.signingID = signingID;
}
}
cd.quarantineURL = fileInfo.quarantineDataURL;
SNTRule *rule = [self.ruleTable ruleForBinarySHA256:cd.sha256
signingID:signingID
signingID:cd.signingID
certificateSHA256:cd.certSHA256
teamID:teamID];
teamID:cd.teamID];
if (rule) {
switch (rule.type) {
case SNTRuleTypeBinary:
@@ -103,13 +108,14 @@
case SNTRuleStateSilentBlock: cd.silentBlock = YES;
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockBinary;
return cd;
case SNTRuleStateAllowCompiler:
// If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules
// become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if
// it were SNTRuleStateAllow.
if ([[SNTConfigurator configurator] enableTransitiveRules]) {
if ([self.configurator enableTransitiveRules]) {
cd.decision = SNTEventStateAllowCompiler;
} else {
cd.decision = SNTEventStateAllowBinary;
@@ -119,7 +125,7 @@
// If transitive rules are enabled, then SNTRuleStateAllowTransitive
// rules become SNTEventStateAllowTransitive decisions. Otherwise, we treat the
// rule as if it were SNTRuleStateUnknown.
if ([[SNTConfigurator configurator] enableTransitiveRules]) {
if ([self.configurator enableTransitiveRules]) {
cd.decision = SNTEventStateAllowTransitive;
return cd;
} else {
@@ -131,11 +137,22 @@
case SNTRuleTypeSigningID:
switch (rule.state) {
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowSigningID; return cd;
case SNTRuleStateAllowCompiler:
// If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules
// become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if
// it were SNTRuleStateAllowSigningID.
if ([self.configurator enableTransitiveRules]) {
cd.decision = SNTEventStateAllowCompiler;
} else {
cd.decision = SNTEventStateAllowSigningID;
}
return cd;
case SNTRuleStateSilentBlock:
cd.silentBlock = YES;
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockSigningID;
return cd;
default: break;
@@ -149,6 +166,7 @@
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockCertificate;
return cd;
default: break;
@@ -162,6 +180,7 @@
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockTeamID;
return cd;
default: break;
@@ -194,9 +213,6 @@
return cd;
}
SNTClientMode mode = [[SNTConfigurator configurator] clientMode];
cd.decisionClientMode = mode;
switch (mode) {
case SNTClientModeMonitor: cd.decision = SNTEventStateAllowUnknown; return cd;
case SNTClientModeLockdown: cd.decision = SNTEventStateBlockUnknown; return cd;

View File

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

View File

@@ -37,10 +37,12 @@
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/santad/SNTDaemonControlController.h"
#include "Source/santad/SNTDecisionCache.h"
#include "Source/santad/TTYWriter.h"
using santa::common::PrefixTree;
using santa::common::Unit;
using santa::santad::Metrics;
using santa::santad::TTYWriter;
using santa::santad::data_layer::WatchItems;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
@@ -79,7 +81,8 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
MOLXPCConnection *control_connection, SNTCompilerController *compiler_controller,
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller,
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree) {
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree,
std::shared_ptr<TTYWriter> tty_writer) {
SNTConfigurator *configurator = [SNTConfigurator configurator];
SNTDaemonControlController *dc =
@@ -133,20 +136,23 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
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]];
[[SNTEndpointSecurityFileAccessAuthorizer alloc] initWithESAPI:esapi
metrics:metrics
logger:logger
watchItems:watch_items
enricher:enricher
decisionCache:[SNTDecisionCache sharedCache]
ttyWriter:tty_writer];
watch_items->RegisterClient(access_authorizer_client);
access_authorizer_client.fileAccessBlockCallback = ^(SNTFileAccessEvent *event) {
[[notifier_queue.notifierConnection remoteObjectProxy]
postFileAccessBlockNotification:event
withCustomMessage:@"Access to the resource has been denied!"];
};
access_authorizer_client.fileAccessBlockCallback =
^(SNTFileAccessEvent *event, NSString *customMsg, NSString *customURL, NSString *customText) {
[[notifier_queue.notifierConnection remoteObjectProxy]
postFileAccessBlockNotification:event
customMessage:customMsg
customURL:customURL
customText:customText];
};
}
EstablishSyncServiceConnection(syncd_queue);

View File

@@ -34,6 +34,7 @@
#import "Source/santad/SNTExecutionController.h"
#import "Source/santad/SNTNotificationQueue.h"
#import "Source/santad/SNTSyncdQueue.h"
#include "Source/santad/TTYWriter.h"
namespace santa::santad {
@@ -56,7 +57,8 @@ class SantadDeps {
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller,
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>
prefix_tree);
prefix_tree,
std::shared_ptr<santa::santad::TTYWriter> tty_writer);
std::shared_ptr<santa::santad::event_providers::AuthResultCache>
AuthResultCache();
@@ -74,6 +76,7 @@ class SantadDeps {
SNTSyncdQueue *SyncdQueue();
SNTExecutionController *ExecController();
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> PrefixTree();
std::shared_ptr<santa::santad::TTYWriter> TTYWriter();
private:
std::shared_ptr<
@@ -93,6 +96,7 @@ class SantadDeps {
SNTSyncdQueue *syncd_queue_;
SNTExecutionController *exec_controller_;
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree_;
std::shared_ptr<santa::santad::TTYWriter> tty_writer_;
};
} // namespace santa::santad

View File

@@ -84,7 +84,7 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
exit(EXIT_FAILURE);
}
std::shared_ptr<TTYWriter> tty_writer = TTYWriter::Create();
std::shared_ptr<::TTYWriter> tty_writer = TTYWriter::Create();
if (!tty_writer) {
LOGW(@"Unable to initialize TTY writer");
}
@@ -152,10 +152,10 @@ 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), std::move(auth_result_cache),
control_connection, compiler_controller, notifier_queue,
syncd_queue, exec_controller, prefix_tree);
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, std::move(tty_writer));
}
SantadDeps::SantadDeps(
@@ -164,7 +164,8 @@ SantadDeps::SantadDeps(
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)
SNTExecutionController *exec_controller, std::shared_ptr<::PrefixTree<Unit>> prefix_tree,
std::shared_ptr<::TTYWriter> tty_writer)
: esapi_(std::move(esapi)),
logger_(std::move(logger)),
metrics_(std::move(metrics)),
@@ -176,7 +177,8 @@ SantadDeps::SantadDeps(
notifier_queue_(notifier_queue),
syncd_queue_(syncd_queue),
exec_controller_(exec_controller),
prefix_tree_(prefix_tree) {}
prefix_tree_(prefix_tree),
tty_writer_(std::move(tty_writer)) {}
std::shared_ptr<::AuthResultCache> SantadDeps::AuthResultCache() {
return auth_result_cache_;
@@ -225,4 +227,8 @@ std::shared_ptr<PrefixTree<Unit>> SantadDeps::PrefixTree() {
return prefix_tree_;
}
std::shared_ptr<::TTYWriter> SantadDeps::TTYWriter() {
return tty_writer_;
}
} // namespace santa::santad

View File

@@ -24,6 +24,7 @@
#include <memory>
#import "Source/common/SNTCachedDecision.h"
#import "Source/common/SNTConfigurator.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
@@ -31,6 +32,7 @@
#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h"
#import "Source/santad/Metrics.h"
#import "Source/santad/SNTDatabaseController.h"
#import "Source/santad/SNTDecisionCache.h"
#include "Source/santad/SantadDeps.h"
using santa::santad::SantadDeps;
@@ -41,7 +43,7 @@ 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";
static const char *kAllowedTeamID = "TJNVEKW352";
@interface SantadTest : XCTestCase
@property id mockSNTDatabaseController;
@@ -62,10 +64,17 @@ static const char *kNoRuleMatchTeamID = "ABC1234XYZ";
- (BOOL)checkBinaryExecution:(NSString *)binaryName
wantResult:(es_auth_result_t)wantResult
clientMode:(NSInteger)clientMode
cdValidator:(BOOL (^)(SNTCachedDecision *))cdValidator
messageSetup:(void (^)(es_message_t *))messageSetupBlock {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
id mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
OCMStub([mockDecisionCache sharedCache]).andReturn(mockDecisionCache);
if (cdValidator) {
OCMExpect([mockDecisionCache cacheDecision:[OCMArg checkWithBlock:cdValidator]]);
}
id mockConfigurator = OCMClassMock([SNTConfigurator class]);
OCMStub([mockConfigurator configurator]).andReturn(mockConfigurator);
@@ -150,6 +159,8 @@ static const char *kNoRuleMatchTeamID = "ABC1234XYZ";
[self waitForExpectations:@[ expectation ] timeout:10.0];
XCTAssertTrue(OCMVerifyAll(mockDecisionCache), "Unable to verify SNTCachedDecision properties");
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
"Failed waiting for message to be processed...");
@@ -159,10 +170,12 @@ static const char *kNoRuleMatchTeamID = "ABC1234XYZ";
- (BOOL)checkBinaryExecution:(NSString *)binaryName
wantResult:(es_auth_result_t)wantResult
clientMode:(NSInteger)clientMode {
clientMode:(NSInteger)clientMode
cdValidator:(BOOL (^)(SNTCachedDecision *))cdValidator {
return [self checkBinaryExecution:binaryName
wantResult:wantResult
clientMode:clientMode
cdValidator:cdValidator
messageSetup:nil];
}
@@ -174,145 +187,221 @@ static const char *kNoRuleMatchTeamID = "ABC1234XYZ";
- (void)testBinaryWithSHA256BlockRuleIsBlockedInLockdownMode {
[self checkBinaryExecution:@"badbinary"
wantResult:ES_AUTH_RESULT_DENY
clientMode:SNTClientModeLockdown];
clientMode:SNTClientModeLockdown
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateBlockBinary;
}];
}
- (void)testBinaryWithSHA256BlockRuleIsBlockedInMonitorMode {
[self checkBinaryExecution:@"badbinary"
wantResult:ES_AUTH_RESULT_DENY
clientMode:SNTClientModeMonitor];
clientMode:SNTClientModeMonitor
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateBlockBinary;
}];
}
- (void)testBinaryWithSHA256AllowRuleIsNotBlockedInLockdownMode {
[self checkBinaryExecution:@"goodbinary"
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeLockdown];
clientMode:SNTClientModeLockdown
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateAllowBinary;
}];
}
- (void)testBinaryWithSHA256AllowRuleIsNotBlockedInMonitorMode {
[self checkBinaryExecution:@"goodbinary"
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeMonitor];
clientMode:SNTClientModeMonitor
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateAllowBinary;
}];
}
- (void)testBinaryWithCertificateAllowRuleIsNotBlockedInLockdownMode {
[self checkBinaryExecution:@"goodcert"
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeLockdown];
clientMode:SNTClientModeLockdown
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateAllowCertificate;
}];
}
- (void)testBinaryWithCertificateAllowRuleIsNotBlockedInMonitorMode {
[self checkBinaryExecution:@"goodcert"
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeMonitor];
clientMode:SNTClientModeMonitor
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateAllowCertificate;
}];
}
- (void)testBinaryWithCertificateBlockRuleIsBlockedInLockdownMode {
[self checkBinaryExecution:@"badcert"
wantResult:ES_AUTH_RESULT_DENY
clientMode:SNTClientModeLockdown];
clientMode:SNTClientModeLockdown
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateBlockCertificate;
}];
}
- (void)testBinaryWithCertificateBlockRuleIsNotBlockedInMonitorMode {
- (void)testBinaryWithCertificateBlockRuleIsBlockedInMonitorMode {
[self checkBinaryExecution:@"badcert"
wantResult:ES_AUTH_RESULT_DENY
clientMode:SNTClientModeMonitor];
clientMode:SNTClientModeMonitor
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateBlockCertificate;
}];
}
- (void)testBinaryWithTeamIDBlockRuleIsBlockedInLockdownMode {
- (void)testBinaryWithTeamIDAllowRuleAndNoSigningIDMatchIsAllowedInLockdownMode {
[self checkBinaryExecution:@"allowed_teamid"
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeLockdown
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateAllowTeamID;
}
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kAllowedTeamID);
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
}];
}
- (void)testBinaryWithTeamIDAllowRuleAndNoSigningIDMatchIsAllowedInMonitorMode {
[self checkBinaryExecution:@"allowed_teamid"
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeMonitor
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateAllowTeamID;
}
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kAllowedTeamID);
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
}];
}
- (void)testBinaryWithTeamIDBlockRuleAndNoSigningIDMatchIsBlockedInLockdownMode {
[self checkBinaryExecution:@"banned_teamid"
wantResult:ES_AUTH_RESULT_DENY
clientMode:SNTClientModeLockdown];
wantResult:ES_AUTH_RESULT_DENY
clientMode:SNTClientModeLockdown
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateBlockTeamID;
}
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
}];
}
- (void)testBinaryWithTeamIDBlockRuleIsBlockedInMonitorMode {
- (void)testBinaryWithTeamIDBlockRuleAndNoSigningIDMatchIsBlockedInMonitorMode {
[self checkBinaryExecution:@"banned_teamid"
wantResult:ES_AUTH_RESULT_DENY
clientMode:SNTClientModeMonitor];
wantResult:ES_AUTH_RESULT_DENY
clientMode:SNTClientModeMonitor
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateBlockTeamID;
}
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
}];
}
- (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)testBinaryWithSigningIDBlockRuleIsBlockedInLockdownMode {
[self checkBinaryExecution:@"banned_signingid"
wantResult:ES_AUTH_RESULT_DENY
clientMode:SNTClientModeLockdown
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateBlockSigningID;
}
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)testBinaryWithSigningIDBlockRuleIsBlockedInMonitorMode {
[self checkBinaryExecution:@"banned_signingid"
wantResult:ES_AUTH_RESULT_DENY
clientMode:SNTClientModeMonitor
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateBlockSigningID;
}
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
msg->event.exec.target->signing_id = MakeESStringToken(kBlockedSigningID);
}];
}
- (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)testBinaryWithSigningIDAllowRuleIsAllowedInMonitorMode {
[self checkBinaryExecution:@"allowed_signingid"
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeMonitor
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateAllowSigningID;
}
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
msg->event.exec.target->signing_id = MakeESStringToken(kAllowedSigningID);
}];
}
- (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)testBinaryWithSigningIDAllowRuleIsAllowedInLockdownMode {
[self checkBinaryExecution:@"allowed_signingid"
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeMonitor
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateAllowSigningID;
}
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
msg->event.exec.target->signing_id = MakeESStringToken(kAllowedSigningID);
}];
}
- (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInLockdownMode {
[self checkBinaryExecution:@"banned_teamid_allowed_binary"
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeLockdown];
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeLockdown
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateAllowBinary;
}
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
}];
}
- (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInMonitorMode {
[self checkBinaryExecution:@"banned_teamid_allowed_binary"
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeMonitor];
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeMonitor
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateAllowBinary;
}
messageSetup:^(es_message_t *msg) {
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
}];
}
- (void)testBinaryWithoutBlockOrAllowRuleIsAllowedInLockdownMode {
- (void)testBinaryWithoutBlockOrAllowRuleIsBlockedInLockdownMode {
[self checkBinaryExecution:@"noop"
wantResult:ES_AUTH_RESULT_DENY
clientMode:SNTClientModeLockdown];
clientMode:SNTClientModeLockdown
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateBlockUnknown;
}];
}
- (void)testBinaryWithoutBlockOrAllowRuleIsAllowedInMonitorMode {
[self checkBinaryExecution:@"noop"
wantResult:ES_AUTH_RESULT_ALLOW
clientMode:SNTClientModeMonitor];
clientMode:SNTClientModeMonitor
cdValidator:^BOOL(SNTCachedDecision *cd) {
return cd.decision == SNTEventStateAllowUnknown;
}];
}
@end

View File

@@ -15,6 +15,7 @@
#ifndef SANTA__SANTAD__TTYWRITER_H
#define SANTA__SANTAD__TTYWRITER_H
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#include <dispatch/dispatch.h>
@@ -37,7 +38,9 @@ class TTYWriter {
TTYWriter(const TTYWriter &other) = delete;
TTYWriter &operator=(const TTYWriter &other) = delete;
void Write(const char *tty, NSString *msg);
static bool CanWrite(const es_process_t *proc);
void Write(const es_process_t *proc, NSString *msg);
private:
dispatch_queue_t q_;

View File

@@ -16,6 +16,7 @@
#include <string.h>
#include <sys/errno.h>
#include <sys/param.h>
#import "Source/common/SNTLogging.h"
#include "Source/common/String.h"
@@ -24,7 +25,7 @@ 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,
"com.google.santa.ttywriter", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
if (!q) {
@@ -36,20 +37,30 @@ std::unique_ptr<TTYWriter> TTYWriter::Create() {
TTYWriter::TTYWriter(dispatch_queue_t q) : q_(q) {}
void TTYWriter::Write(const char *tty, NSString *msg) {
bool TTYWriter::CanWrite(const es_process_t *proc) {
return proc && proc->tty && proc->tty->path.length > 0;
}
void TTYWriter::Write(const es_process_t *proc, NSString *msg) {
if (!CanWrite(proc)) {
return;
}
// Copy the data from the es_process_t so the ES message doesn't
// need to be retained
NSString *tty = santa::common::StringToNSString(proc->tty->path.data);
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);
int fd = open(tty.UTF8String, 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);
});
}

View File

@@ -153,7 +153,7 @@ int main(int argc, char *argv[]) {
SantadMain(deps->ESAPI(), deps->Logger(), deps->Metrics(), deps->WatchItems(), deps->Enricher(),
deps->AuthResultCache(), deps->ControlConnection(), deps->CompilerController(),
deps->NotifierQueue(), deps->SyncdQueue(), deps->ExecController(),
deps->PrefixTree());
deps->PrefixTree(), deps->TTYWriter());
}
return 0;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -28,7 +28,10 @@
}
- (BOOL)sync {
[self performRequest:[self requestWithDictionary:nil]];
[self performRequest:[self requestWithDictionary:@{
kPostflightRulesReceived : @(self.syncState.rulesReceived),
kPostflightRulesProcessed : @(self.syncState.rulesProcessed),
}]];
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
@@ -69,6 +72,36 @@
}];
}
if (self.syncState.enableBundles) {
[rop setEnableBundles:[self.syncState.enableBundles boolValue]
reply:^{
}];
}
if (self.syncState.enableTransitiveRules) {
[rop setEnableTransitiveRules:[self.syncState.enableTransitiveRules boolValue]
reply:^{
}];
}
if (self.syncState.enableAllEventUpload) {
[rop setEnableAllEventUpload:[self.syncState.enableAllEventUpload boolValue]
reply:^{
}];
}
if (self.syncState.disableUnknownEventUpload) {
[rop setDisableUnknownEventUpload:[self.syncState.disableUnknownEventUpload boolValue]
reply:^{
}];
}
if (self.syncState.overrideFileAccessAction) {
[rop setOverrideFileAccessAction:self.syncState.overrideFileAccessAction
reply:^{
}];
}
// Update last sync success
[rop setFullSyncLastSuccess:[NSDate date]
reply:^{

View File

@@ -24,6 +24,15 @@
#import "Source/santasyncservice/SNTSyncLogging.h"
#import "Source/santasyncservice/SNTSyncState.h"
// Return the given value or nil if not of the expected given class
static id EnsureType(id val, Class c) {
if ([val isKindOfClass:c]) {
return val;
} else {
return nil;
}
}
@implementation SNTSyncPreflight
- (NSURL *)stageURL {
@@ -82,44 +91,31 @@
if (!resp) return NO;
NSNumber *enableBundles = resp[kEnableBundles];
if (!enableBundles) enableBundles = resp[kEnableBundlesDeprecated];
[rop setEnableBundles:[enableBundles boolValue]
reply:^{
}];
self.syncState.enableBundles = EnsureType(resp[kEnableBundles], [NSNumber class])
?: EnsureType(resp[kEnableBundlesDeprecated], [NSNumber class]);
self.syncState.enableTransitiveRules = EnsureType(resp[kEnableTransitiveRules], [NSNumber class])
?: EnsureType(resp[kEnableTransitiveRulesDeprecated], [NSNumber class])
?: EnsureType(resp[kEnableTransitiveRulesSuperDeprecated], [NSNumber class]);
self.syncState.enableAllEventUpload = EnsureType(resp[kEnableAllEventUpload], [NSNumber class]);
self.syncState.disableUnknownEventUpload =
EnsureType(resp[kDisableUnknownEventUpload], [NSNumber class]);
NSNumber *enableTransitiveRules = resp[kEnableTransitiveRules];
if (!enableTransitiveRules) enableTransitiveRules = resp[kEnableTransitiveRulesDeprecated];
if (!enableTransitiveRules) enableTransitiveRules = resp[kEnableTransitiveRulesSuperDeprecated];
BOOL enabled = [enableTransitiveRules boolValue];
[rop setEnableTransitiveRules:enabled
reply:^{
}];
NSNumber *enableAllEventUpload = resp[kEnableAllEventUpload];
[rop setEnableAllEventUpload:[enableAllEventUpload boolValue]
reply:^{
}];
NSNumber *disableUnknownEventUpload = resp[kDisableUnknownEventUpload];
[rop setDisableUnknownEventUpload:[disableUnknownEventUpload boolValue]
reply:^{
}];
self.syncState.eventBatchSize = [resp[kBatchSize] unsignedIntegerValue] ?: kDefaultEventBatchSize;
self.syncState.eventBatchSize =
[EnsureType(resp[kBatchSize], [NSNumber class]) unsignedIntegerValue] ?: kDefaultEventBatchSize;
// Don't let these go too low
NSUInteger FCMIntervalValue = [resp[kFCMFullSyncInterval] unsignedIntegerValue];
self.syncState.pushNotificationsFullSyncInterval = (FCMIntervalValue < kDefaultFullSyncInterval)
? kDefaultPushNotificationsFullSyncInterval
: FCMIntervalValue;
FCMIntervalValue = [resp[kFCMGlobalRuleSyncDeadline] unsignedIntegerValue];
NSUInteger value =
[EnsureType(resp[kFCMFullSyncInterval], [NSNumber class]) unsignedIntegerValue];
self.syncState.pushNotificationsFullSyncInterval =
(value < kDefaultFullSyncInterval) ? kDefaultPushNotificationsFullSyncInterval : value;
value = [EnsureType(resp[kFCMGlobalRuleSyncDeadline], [NSNumber class]) unsignedIntegerValue];
self.syncState.pushNotificationsGlobalRuleSyncDeadline =
(FCMIntervalValue < 60) ? kDefaultPushNotificationsGlobalRuleSyncDeadline : FCMIntervalValue;
(value < 60) ? kDefaultPushNotificationsGlobalRuleSyncDeadline : value;
// Check if our sync interval has changed
NSUInteger intervalValue = [resp[kFullSyncInterval] unsignedIntegerValue];
self.syncState.fullSyncInterval = (intervalValue < 60) ? kDefaultFullSyncInterval : intervalValue;
value = [EnsureType(resp[kFullSyncInterval], [NSNumber class]) unsignedIntegerValue];
self.syncState.fullSyncInterval = (value < 60) ? kDefaultFullSyncInterval : value;
if ([resp[kClientMode] isEqual:kClientModeMonitor]) {
self.syncState.clientMode = SNTClientModeMonitor;
@@ -127,27 +123,21 @@
self.syncState.clientMode = SNTClientModeLockdown;
}
if ([resp[kAllowedPathRegex] isKindOfClass:[NSString class]]) {
self.syncState.allowlistRegex = resp[kAllowedPathRegex];
} else if ([resp[kAllowedPathRegexDeprecated] isKindOfClass:[NSString class]]) {
self.syncState.allowlistRegex = resp[kAllowedPathRegexDeprecated];
}
self.syncState.allowlistRegex =
EnsureType(resp[kAllowedPathRegex], [NSString class])
?: EnsureType(resp[kAllowedPathRegexDeprecated], [NSString class]);
if ([resp[kBlockedPathRegex] isKindOfClass:[NSString class]]) {
self.syncState.blocklistRegex = resp[kBlockedPathRegex];
} else if ([resp[kBlockedPathRegexDeprecated] isKindOfClass:[NSString class]]) {
self.syncState.blocklistRegex = resp[kBlockedPathRegexDeprecated];
}
self.syncState.blocklistRegex =
EnsureType(resp[kBlockedPathRegex], [NSString class])
?: EnsureType(resp[kBlockedPathRegexDeprecated], [NSString class]);
if ([resp[kBlockUSBMount] isKindOfClass:[NSNumber class]]) {
self.syncState.blockUSBMount = resp[kBlockUSBMount];
}
self.syncState.blockUSBMount = EnsureType(resp[kBlockUSBMount], [NSNumber class]);
self.syncState.remountUSBMode = EnsureType(resp[kRemountUSBMode], [NSArray class]);
if ([resp[kRemountUSBMode] isKindOfClass:[NSArray class]]) {
self.syncState.remountUSBMode = resp[kRemountUSBMode];
}
self.syncState.overrideFileAccessAction =
EnsureType(resp[kOverrideFileAccessAction], [NSString class]);
if ([resp[kCleanSync] boolValue]) {
if ([EnsureType(resp[kCleanSync], [NSNumber class]) boolValue]) {
SLOGD(@"Clean sync requested by server");
self.syncState.cleanSync = YES;
}

View File

@@ -79,6 +79,7 @@
// Note that rules from the server are filtered. We only keep those whose rule_type
// is either BINARY or CERTIFICATE. PACKAGE rules are dropped.
- (NSArray<SNTRule *> *)downloadNewRulesFromServer {
self.syncState.rulesReceived = 0;
NSMutableArray<SNTRule *> *newRules = [NSMutableArray array];
NSString *cursor = nil;
do {
@@ -90,18 +91,24 @@
return nil;
}
uint32_t count = 0;
for (NSDictionary *ruleDict in response[kRules]) {
NSArray<NSDictionary *> *rules = response[kRules];
for (NSDictionary *ruleDict in rules) {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:ruleDict];
if (rule) {
[self processBundleNotificationsForRule:rule fromDictionary:ruleDict];
[newRules addObject:rule];
count++;
if (!rule) {
SLOGD(@"Ignoring bad rule: %@", ruleDict);
continue;
}
[self processBundleNotificationsForRule:rule fromDictionary:ruleDict];
[newRules addObject:rule];
}
SLOGI(@"Received %u rules", count);
SLOGI(@"Received %lu rules", (unsigned long)rules.count);
cursor = response[kCursor];
self.syncState.rulesReceived += rules.count;
} while (cursor);
self.syncState.rulesProcessed = newRules.count;
return newRules;
}

View File

@@ -17,6 +17,7 @@
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTXPCControlInterface.h"
@@ -94,11 +95,40 @@
[req setValue:contentEncodingHeader forHTTPHeaderField:@"Content-Encoding"];
}
[self addExtraRequestHeaders:req];
[req setHTTPBody:requestBody];
return req;
}
- (void)addExtraRequestHeaders:(NSMutableURLRequest *)req {
NSDictionary *extra = [[SNTConfigurator configurator] syncExtraHeaders];
[extra enumerateKeysAndObjectsWithOptions:0
usingBlock:^(id key, id object, BOOL *stop) {
if (![key isKindOfClass:[NSString class]] ||
![object isKindOfClass:[NSString class]])
return;
NSString *k = (NSString *)key;
NSString *v = (NSString *)object;
// This is likely unnecessary as the docs for NSURLSession list
// most of these as being ignored but explicitly setting them
// here is an extra layer of protection.
if ([k isEqualToString:@"Content-Encoding"] ||
[k isEqualToString:@"Content-Length"] ||
[k isEqualToString:@"Content-Type"] ||
[k isEqualToString:@"Connection"] ||
[k isEqualToString:@"Host"] ||
[k isEqualToString:@"Proxy-Authenticate"] ||
[k isEqualToString:@"Proxy-Authorization"] ||
[k isEqualToString:@"WWW-Authenticate"])
return;
[req setValue:v forHTTPHeaderField:k];
}];
}
// Returns nil when there is a server connection issue. For other errors, such as
// an empty response or an unparseable response, an empty dictionary is returned.
- (NSDictionary *)performRequest:(NSURLRequest *)request timeout:(NSTimeInterval)timeout {
@@ -106,13 +136,16 @@
NSError *error;
NSData *data;
for (int attempt = 1; attempt < 6; ++attempt) {
if (attempt > 1) {
struct timespec ts = {.tv_sec = (attempt * 2)};
int maxAttempts = 5;
for (int attempt = 1; attempt <= maxAttempts; ++attempt) {
if (attempt >= 2) {
// Exponentially back off with larger and larger delays.
int exponentialBackoffMultiplier = 2; // E.g. 2^2 = 4, 2^3 = 8, 2^4 = 16...
struct timespec ts = {.tv_sec = pow(exponentialBackoffMultiplier, attempt)};
nanosleep(&ts, NULL);
}
SLOGD(@"Performing request, attempt %d", attempt);
SLOGD(@"Performing request, attempt %d (of %d maximum)...", attempt, maxAttempts);
data = [self performRequest:request timeout:timeout response:&response error:&error];
if (response.statusCode == 200) break;

View File

@@ -61,9 +61,14 @@
@property SNTClientMode clientMode;
@property NSString *allowlistRegex;
@property NSString *blocklistRegex;
@property NSNumber *enableBundles;
@property NSNumber *enableTransitiveRules;
@property NSNumber *enableAllEventUpload;
@property NSNumber *disableUnknownEventUpload;
@property NSNumber *blockUSBMount;
// Array of mount args for the forced remounting feature.
@property NSArray *remountUSBMode;
@property NSString *overrideFileAccessAction;
/// Clean sync flag, if True, all existing rules should be deleted before inserting any new rules.
@property BOOL cleanSync;
@@ -77,4 +82,8 @@
/// The content-encoding to use for the client uploads during the sync session.
@property SNTSyncContentEncoding contentEncoding;
/// Counts of rules received and processed during rule download.
@property NSUInteger rulesReceived;
@property NSUInteger rulesProcessed;
@end

View File

@@ -12,6 +12,7 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
@@ -281,6 +282,7 @@
XCTAssertEqual(self.syncState.eventBatchSize, 100);
XCTAssertNil(self.syncState.allowlistRegex);
XCTAssertNil(self.syncState.blocklistRegex);
XCTAssertNil(self.syncState.overrideFileAccessAction);
}
- (void)testPreflightTurnOnBlockUSBMount {
@@ -318,6 +320,32 @@
XCTAssertNil(self.syncState.blockUSBMount);
}
- (void)testPreflightOverrideFileAccessAction {
[self setupDefaultDaemonConnResponses];
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
NSData *respData = [@"{\"override_file_access_action\": \"AuditOnly\", \"client_mode\": "
@"\"LOCKDOWN\", \"batch_size\": 100}" dataUsingEncoding:NSUTF8StringEncoding];
[self stubRequestBody:respData response:nil error:nil validateBlock:nil];
XCTAssertTrue([sut sync]);
XCTAssertEqualObjects(self.syncState.overrideFileAccessAction, @"AuditOnly");
}
- (void)testPreflightOverrideFileAccessActionAbsent {
[self setupDefaultDaemonConnResponses];
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
NSData *respData = [@"{\"client_mode\": \"LOCKDOWN\", \"batch_size\": 100}"
dataUsingEncoding:NSUTF8StringEncoding];
[self stubRequestBody:respData response:nil error:nil validateBlock:nil];
XCTAssertTrue([sut sync]);
XCTAssertNil(self.syncState.overrideFileAccessAction);
}
- (void)testPreflightDatabaseCounts {
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
@@ -603,6 +631,10 @@
self.syncState.blockUSBMount = @0;
XCTAssertTrue([sut sync]);
OCMVerify([self.daemonConnRop setBlockUSBMount:NO reply:OCMOCK_ANY]);
self.syncState.overrideFileAccessAction = @"Disable";
XCTAssertTrue([sut sync]);
OCMVerify([self.daemonConnRop setOverrideFileAccessAction:@"Disable" reply:OCMOCK_ANY]);
}
@end

View File

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

View File

@@ -27,8 +27,6 @@
<true/>
<key>EnablePageZeroProtection</key>
<false/>
<key>EnableSystemExtension</key>
<true/>
<key>EventDetailText</key>
<string>Open in moroz...</string>
<key>EventDetailURL</key>

View File

@@ -27,8 +27,6 @@
<true/>
<key>EnablePageZeroProtection</key>
<false/>
<key>EnableSystemExtension</key>
<true/>
<key>EventDetailText</key>
<string>Open in moroz...</string>
<key>EventDetailURL</key>

View File

@@ -1,25 +1,14 @@
#!/bin/bash
set -xo pipefail
function main() {
GIT_ROOT=$(git rev-parse --show-toplevel)
err=0
GIT_ROOT=$(git rev-parse --show-toplevel)
find $GIT_ROOT \( -name "*.m" -o -name "*.h" -o -name "*.mm" -o -name "*.cc" \) -exec clang-format --Werror --dry-run {} \+
err="$(( $err | $? ))"
find $GIT_ROOT \( -name "*.m" -o -name "*.h" -o -name "*.mm" -o -name "*.cc" \) -exec clang-format --Werror --dry-run {} \+
! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'
err="$(( $err | $? ))"
! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'
go install github.com/bazelbuild/buildtools/buildifier@latest
~/go/bin/buildifier --lint=warn -r $GIT_ROOT
err="$(( $err | $? ))"
go install github.com/bazelbuild/buildtools/buildifier@latest
~/go/bin/buildifier --lint=warn -r $GIT_ROOT
python3 -m pip install -q pylint
find $GIT_ROOT \( -name "*.py" \) -exec python3 -m pylint --score n --rcfile=$GIT_ROOT/.pylintrc {} \+
err="$(( $err | $? ))"
return $err
}
main $@
exit $?
python3 -m pip install -q pylint
find $GIT_ROOT \( -name "*.py" \) -exec python3 -m pylint --score n --rcfile=$GIT_ROOT/.pylintrc {} \+

View File

@@ -7,6 +7,22 @@ load(
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# Abseil LTS branch, Aug 2023
http_archive(
name = "com_google_absl",
strip_prefix = "abseil-cpp-20230802.0",
urls = ["https://github.com/abseil/abseil-cpp/archive/refs/tags/20230802.0.tar.gz"],
)
http_archive(
name = "com_google_protobuf",
patch_args = ["-p1"],
patches = ["//external_patches/com_google_protobuf:13636.patch"],
sha256 = "07d69502e58248927b58c7d7e7424135272ba5b2852a753ab6b67e62d2d29355",
strip_prefix = "protobuf-24.3",
urls = ["https://github.com/protocolbuffers/protobuf/archive/v24.3.tar.gz"],
)
# We don't directly use rules_python but several dependencies do and they disagree
# about which version to use, so we force the latest.
http_archive(
@@ -18,28 +34,15 @@ http_archive(
http_archive(
name = "build_bazel_rules_apple",
sha256 = "f003875c248544009c8e8ae03906bbdacb970bc3e5931b40cd76cadeded99632", # 1.1.0
urls = ["https://github.com/bazelbuild/rules_apple/releases/download/1.1.0/rules_apple.1.1.0.tar.gz"],
sha256 = "8ac4c7997d863f3c4347ba996e831b5ec8f7af885ee8d4fe36f1c3c8f0092b2c",
url = "https://github.com/bazelbuild/rules_apple/releases/download/2.5.0/rules_apple.2.5.0.tar.gz",
)
load("@build_bazel_rules_apple//apple:repositories.bzl", "apple_rules_dependencies")
apple_rules_dependencies()
load("@build_bazel_apple_support//lib:repositories.bzl", "apple_support_dependencies")
apple_support_dependencies()
http_archive(
name = "build_bazel_rules_swift",
sha256 = "84e2cc1c9e3593ae2c0aa4c773bceeb63c2d04c02a74a6e30c1961684d235593",
url = "https://github.com/bazelbuild/rules_swift/releases/download/1.5.1/rules_swift.1.5.1.tar.gz",
)
load(
"@build_bazel_rules_swift//swift:repositories.bzl",
"swift_rules_dependencies",
)
load("@build_bazel_rules_swift//swift:repositories.bzl", "swift_rules_dependencies")
swift_rules_dependencies()
@@ -50,6 +53,10 @@ load(
swift_rules_extra_dependencies()
load("@build_bazel_apple_support//lib:repositories.bzl", "apple_support_dependencies")
apple_support_dependencies()
# Hedron Bazel Compile Commands Extractor
# Allows integrating with clangd
# https://github.com/hedronvision/bazel-compile-commands-extractor
@@ -72,23 +79,6 @@ http_archive(
urls = ["https://github.com/google/googletest/archive/58d77fa8070e8cec2dc1ed015d66b454c8d78850.zip"],
)
# Abseil - Abseil LTS branch, June 2022, Patch 1
http_archive(
name = "com_google_absl",
sha256 = "b9f490fae1c0d89a19073a081c3c588452461e5586e4ae31bc50a8f36339135e",
strip_prefix = "abseil-cpp-8c0b94e793a66495e0b1f34a5eb26bd7dc672db0",
urls = ["https://github.com/abseil/abseil-cpp/archive/8c0b94e793a66495e0b1f34a5eb26bd7dc672db0.zip"],
)
http_archive(
name = "com_google_protobuf",
patch_args = ["-p1"],
patches = ["//external_patches/com_google_protobuf:10120.patch"],
sha256 = "73c95c7b0c13f597a6a1fec7121b07e90fd12b4ed7ff5a781253b3afe07fc077",
strip_prefix = "protobuf-3.21.6",
urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.21.6.tar.gz"],
)
# Note: Protobuf deps must be loaded after defining the ABSL archive since
# protobuf repo would pull an in earlier version of ABSL.
load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
@@ -195,8 +185,8 @@ git_repository(
shallow_since = "1594986926 -0400",
)
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
load("//external_patches/moroz:deps.bzl", "moroz_dependencies")
# gazelle:repository_macro external_patches/moroz/deps.bzl%moroz_dependencies

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