Compare commits

...

31 Commits

Author SHA1 Message Date
Russell Hancox
7502bc247f santactl/fileinfo: Include teamID/platform prefix in signing ID (#1356) 2024-05-21 12:48:48 -04:00
Russell Hancox
cf4dab55e0 santactl/rule: Allow adding signing ID and team ID rules by file path (#1357) 2024-05-21 12:48:27 -04:00
Matt W
e43ad30d4e Fix NSSecureCoding adoption in SNTFileAccessEvent (#1358) 2024-05-21 11:35:07 -04:00
np5
d8928ac320 Add cdhash, teamID, and signingID to bundle events (#1353)
Fix #1352
2024-05-20 10:45:36 -04:00
Matt W
ac1c9d8b05 Fix stat metrics accounting. Refactor setting metrics to be more general. (#1354) 2024-05-17 12:15:48 -04:00
Matt W
9b184ed4fb Add metric for when the file on disk is not the file being evaluated (#1348)
* Add metrics for stat change detection

* Fix test related issues due to partially constructed messages

* lint

* Convert errno to enum class StatResult

* Cleanup from PR feedback
2024-05-16 16:13:29 -04:00
Russell Hancox
67883c5200 GUI: Fix unicode rendering of attributed messages (#1351)
Also added a test to stop this from happening again
2024-05-15 16:27:28 -04:00
Russell Hancox
8e1e155c23 Project: Re-enable layering_checks (#1350) 2024-05-15 14:05:58 -04:00
Russell Hancox
fb6aa850b3 santad: Drop QoS of notify handling queue (#1349)
Bumping from BACKGROUND to DEFAULT had the desired impact of processing events faster and reducing memory usage but had a larger-than-expected increase in CPU usage. UTILITY is in the middle of these two and better fits the desired priority.
2024-05-15 11:53:31 -04:00
czcx
7f06b8c11a Docs: Minor grammar & correctness fixes in known-limitations.md. (#1345) 2024-05-14 13:06:45 -04:00
Matt W
978b33e450 Adopt --preserve-metadata flag to simplify resigning with entitlements (#1346) 2024-05-10 13:40:19 -04:00
Russell Hancox
f00ad32edd santad: Bump QoS of notify handling queue (#1342)
The use of the background queue is a historical artifact from when Santa had its own kernel extension with separate in-kernel queues for processing AUTH & NOTIFY type events. With the move to ES and the larger number of event types that we now notify on, running at the background QoS carries a small risk that the thread processing these events is not given a chance to run often enough that the queue grows and increases memory usage.
2024-05-09 15:44:14 -04:00
Pete Markowsky
7b0d2fdbb8 Add necessary dep for SNTPolicyProcessorTest (#1343) 2024-05-09 15:29:17 -04:00
Russell Hancox
1672e52b7b Project: Disable layering_check in all BUILD files (#1344) 2024-05-09 15:25:19 -04:00
Pete Markowsky
6cca5ab27d Update SNTPolicyProcessor to use a map (#1304)
* Update SNTPolicyProcessor to use a map instead of a giant switch statement

Update SNTPolicyProcessor to use a map instead of a giant switch statement.

Add unit tests for the method that sets SNTCachedDecision values.

* Remove unneccessary OCMock dep in BUILD file.

* Fix typo in method signature.

* Incorporate review feedback.

* Upper case UpdateCachedDecisionSigningInfo

* Update SNTPolicyProcessor.h

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

* Update SNTPolicyProcessor.mm

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

* Fix typo

* Fix linter issues.

* Fixed up more linter issues.

---------

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
2024-05-09 14:38:12 -04:00
Matt W
7e4af5e337 Update to Abseil 20240116.1. Fix includes. (#1341) 2024-05-09 12:33:46 -04:00
Russell Hancox
5ea4431901 Project: Move fuzzing rules to bzlmod, fix santa_unit_test (#1339) 2024-05-08 11:50:04 -04:00
Tom Burgin
b53818f556 SNTBlockMessage: add more template options (#1337)
* update event detail url

* refactor template mappings

* re-enable testEventDetailURLForFileAccessEvent

* null

* missed one

* update comment
2024-05-07 09:20:50 -04:00
Tom Burgin
0f5e551345 Project: Fix lint.sh to bubble up all errors, switch from pylint to pyink, fix existing lint errors (#1338) 2024-05-06 16:47:32 -04:00
Nick Gregory
51b0f7146d Testing: update E2E to use JIT runners (#1335)
* inject jit runner token into e2e vm

* split out vm updating

* argparse + logging

* restore update/start vm steps

* pr review comments

* rm gcp.json and verify runner sha
2024-05-06 09:10:04 -04:00
czcx
f5882b3146 docs: Fix grammar and typo in syncing-overview 2024-04-30 12:58:34 -04:00
Rohan Sharma
59c146b4af README: Fix typo in landing page (#1332) 2024-04-30 12:25:53 -04:00
czcx
aaa2b0e259 Docs: Grammar updates on doc index 2024-04-29 17:23:39 -04:00
czcx
9c6fd0677f README: Minor grammar issue fix (#1329) 2024-04-29 15:53:52 -04:00
Russell Hancox
344a35aaf6 Project: Migrate to bazel modules (#1324)
This includes updating to rules_apple 3.5.1 and protobuf 26.1, as well as updating several tests to no longer use the data attribute to pass in testdata.
2024-04-11 17:19:30 -04:00
Matt W
45e36fa501 Bump protobuf to v26.1, update to use new interfaces. (#1317) 2024-04-11 14:22:43 -04:00
Nick Gregory
d5a7c5f1fa ProcessTree: add the first annotation, originator (4/4) (#1296) 2024-04-11 13:35:53 -04:00
Pete Markowsky
22aca6b505 Add macOS-14 to the test matrix. (#1323) 2024-04-05 15:09:04 -04:00
Pete Markowsky
375f7bd9cc Fix: Update code to use the new MOLCodesignChecker interfaces for codesigning info (#1322)
* Update code to use the  new MOLCodesignChecker interfaces for codesigning info.
2024-04-05 12:27:33 -04:00
Matt W
7d58665e87 Bump MOLCodesignChecker tag to latest (#1321) 2024-04-05 10:39:08 -04:00
Ryan Diers
3b2d02f38d GUI: Restore default button type to MessageWindow for blocked events (#1316) 2024-03-28 15:02:01 -04:00
64 changed files with 1880 additions and 1245 deletions

View File

@@ -1 +1 @@
6.3.2
7.0.0

View File

@@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-11, macos-12, macos-13]
os: [macos-11, macos-12, macos-13, macos-14]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
@@ -31,14 +31,14 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-11, macos-12, macos-13]
os: [macos-11, macos-12, macos-13, macos-14]
runs-on: ${{ matrix.os }}
steps:
- 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
runs-on: macos-14
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Generate test coverage

View File

@@ -6,11 +6,29 @@ on:
workflow_dispatch:
jobs:
update_vm:
runs-on: e2e-host
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Update VM
env:
GCS_KEY: ${{ secrets.GCS_SERVICE_ACCOUNT_KEY }}
run: |
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json
echo "${GCS_KEY}" > ${GOOGLE_APPLICATION_CREDENTIALS}
function cleanup {
rm /tmp/gcp.json
}
trap cleanup EXIT
python3 Testing/integration/actions/update_vm.py macOS_14.bundle.tar.gz
start_vm:
runs-on: e2e-host
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Start VM
env:
RUNNER_REG_TOKEN: ${{ secrets.RUNNER_REG_TOKEN }}
run: python3 Testing/integration/actions/start_vm.py macOS_14.bundle.tar.gz
integration:

2
.gitignore vendored
View File

@@ -2,7 +2,7 @@
*.profraw
*.provisionprofile
bazel-*
Pods
MODULE.bazel.lock
Santa.xcodeproj/*
Santa.xcworkspace/*
CoverageData/*

5
.pyink-config Normal file
View File

@@ -0,0 +1,5 @@
[tool.pyink]
pyink = true
line-length = 80
pyink-indentation = 2
pyink-use-majority-quotes = true

429
.pylintrc
View File

@@ -1,429 +0,0 @@
# This Pylint rcfile contains a best-effort configuration to uphold the
# best-practices and style described in the Google Python style guide:
# https://google.github.io/styleguide/pyguide.html
#
# Its canonical open-source location is:
# https://google.github.io/styleguide/pylintrc
[MASTER]
# Files or directories to be skipped. They should be base names, not paths.
ignore=third_party
# Files or directories matching the regex patterns are skipped. The regex
# matches against base names, not paths.
ignore-patterns=
# Pickle collected data for later comparisons.
persistent=no
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=4
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=abstract-method,
apply-builtin,
arguments-differ,
attribute-defined-outside-init,
backtick,
bad-option-value,
basestring-builtin,
buffer-builtin,
c-extension-no-member,
consider-using-enumerate,
cmp-builtin,
cmp-method,
coerce-builtin,
coerce-method,
delslice-method,
div-method,
duplicate-code,
eq-without-hash,
execfile-builtin,
file-builtin,
filter-builtin-not-iterating,
fixme,
getslice-method,
global-statement,
hex-method,
idiv-method,
implicit-str-concat,
import-error,
import-self,
import-star-module-level,
inconsistent-return-statements,
input-builtin,
intern-builtin,
invalid-str-codec,
locally-disabled,
long-builtin,
long-suffix,
map-builtin-not-iterating,
misplaced-comparison-constant,
missing-function-docstring,
metaclass-assignment,
next-method-called,
next-method-defined,
no-absolute-import,
no-else-break,
no-else-continue,
no-else-raise,
no-else-return,
no-init, # added
no-member,
no-name-in-module,
no-self-use,
nonzero-method,
oct-method,
old-division,
old-ne-operator,
old-octal-literal,
old-raise-syntax,
parameter-unpacking,
print-statement,
raising-string,
range-builtin-not-iterating,
raw_input-builtin,
rdiv-method,
reduce-builtin,
relative-import,
reload-builtin,
round-builtin,
setslice-method,
signature-differs,
standarderror-builtin,
suppressed-message,
sys-max-int,
too-few-public-methods,
too-many-ancestors,
too-many-arguments,
too-many-boolean-expressions,
too-many-branches,
too-many-instance-attributes,
too-many-locals,
too-many-nested-blocks,
too-many-public-methods,
too-many-return-statements,
too-many-statements,
trailing-newlines,
unichr-builtin,
unicode-builtin,
unnecessary-pass,
unpacking-in-except,
useless-else-on-loop,
useless-object-inheritance,
useless-suppression,
using-cmp-argument,
wrong-import-order,
xrange-builtin,
zip-builtin-not-iterating,
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[BASIC]
# Good variable names which should always be accepted, separated by a comma
good-names=main,_
# Bad variable names which should always be refused, separated by a comma
bad-names=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
# Regular expression matching correct function names
function-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
# Regular expression matching correct variable names
variable-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct constant names
const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
# Regular expression matching correct attribute names
attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
# Regular expression matching correct argument names
argument-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct class attribute names
class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
# Regular expression matching correct inline iteration names
inlinevar-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct class names
class-rgx=^_?[A-Z][a-zA-Z0-9]*$
# Regular expression matching correct module names
module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$
# Regular expression matching correct method names
method-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=10
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=80
# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt
# lines made too long by directives to pytype.
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=(?x)(
^\s*(\#\ )?<?https?://\S+>?$|
^\s*(from\s+\S+\s+)?import\s+.+$)
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=yes
# Maximum number of lines in a module
max-module-lines=99999
# String used as indentation unit. The internal Google style guide mandates 2
# spaces. Google's externaly-published style guide says 4, consistent with
# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google
# projects (like TensorFlow).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=TODO
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=yes
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging,absl.logging,tensorflow.io.logging
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,
TERMIOS,
Bastion,
rexec,
sets
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant, absl
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls,
class_
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=StandardError,
Exception,
BaseException

4
BUILD
View File

@@ -1,7 +1,9 @@
load("@build_bazel_rules_apple//apple:versioning.bzl", "apple_bundle_version")
load("//:helper.bzl", "run_command")
package(default_visibility = ["//:santa_package_group"])
package(
default_visibility = ["//:santa_package_group"],
)
licenses(["notice"])

View File

@@ -53,11 +53,10 @@ readonly RELEASE_NAME="santa-$(/usr/bin/defaults read "${INPUT_APP}/Contents/Inf
readonly SCRATCH=$(/usr/bin/mktemp -d "${TMPDIR}/santa-"XXXXXX)
readonly APP_PKG_ROOT="${SCRATCH}/app_pkg_root"
readonly APP_PKG_SCRIPTS="${SCRATCH}/pkg_scripts"
readonly ENTITLEMENTS="${SCRATCH}/entitlements"
readonly SCRIPT_PATH="$(/usr/bin/dirname -- ${BASH_SOURCE[0]})"
/bin/mkdir -p "${APP_PKG_ROOT}" "${APP_PKG_SCRIPTS}" "${ENTITLEMENTS}"
/bin/mkdir -p "${APP_PKG_ROOT}" "${APP_PKG_SCRIPTS}"
readonly DMG_PATH="${ARTIFACTS_DIR}/${RELEASE_NAME}.dmg"
readonly TAR_PATH="${ARTIFACTS_DIR}/${RELEASE_NAME}.tar.gz"
@@ -65,19 +64,9 @@ readonly TAR_PATH="${ARTIFACTS_DIR}/${RELEASE_NAME}.tar.gz"
# Sign all of binaries/bundles. Maintain inside-out ordering where necessary
for ARTIFACT in "${INPUT_SANTACTL}" "${INPUT_SANTABS}" "${INPUT_SANTAMS}" "${INPUT_SANTASS}" "${INPUT_SYSX}" "${INPUT_APP}"; do
BN=$(/usr/bin/basename "${ARTIFACT}")
EN="${ENTITLEMENTS}/${BN}.entitlements"
echo "extracting ${BN} entitlements"
/usr/bin/codesign -d --entitlements "${EN}" "${ARTIFACT}"
if [[ -s "${EN}" ]]; then
EN="--entitlements ${EN}"
else
EN=""
fi
echo "codesigning ${BN}"
/usr/bin/codesign --sign "${SIGNING_IDENTITY}" --keychain "${SIGNING_KEYCHAIN}" \
${EN} --timestamp --force --generate-entitlement-der \
--preserve-metadata=entitlements --timestamp --force --generate-entitlement-der \
--options library,kill,runtime "${ARTIFACT}"
done

55
MODULE.bazel Normal file
View File

@@ -0,0 +1,55 @@
module(name = "santa")
bazel_dep(name = "apple_support", version = "1.15.1", repo_name = "build_bazel_apple_support")
bazel_dep(name = "abseil-cpp", version = "20240116.1", repo_name = "com_google_absl")
bazel_dep(name = "rules_python", version = "0.31.0")
bazel_dep(name = "rules_cc", version = "0.0.9")
bazel_dep(name = "rules_apple", version = "3.5.0", repo_name = "build_bazel_rules_apple")
bazel_dep(name = "rules_swift", version = "1.18.0", repo_name = "build_bazel_rules_swift")
bazel_dep(name = "rules_fuzzing", version = "0.5.1")
bazel_dep(name = "protobuf", version = "main", repo_name = "com_google_protobuf")
git_override(
module_name = "protobuf",
commit = "21d75f861cdbc03b0a6b235a9ccf3ba0e1f09b32",
remote = "https://github.com/protocolbuffers/protobuf.git",
)
bazel_dep(name = "googletest", version = "1.14.0.bcr.1", repo_name = "com_google_googletest")
bazel_dep(name = "molcertificate", version = "2.1", repo_name = "MOLCertificate")
git_override(
module_name = "molcertificate",
commit = "34f0ccf68a34a07cc636ada89057c529f90bec3a",
remote = "https://github.com/google/macops-molcertificate.git",
)
bazel_dep(name = "molauthenticatingurlsession", version = "3.0", repo_name = "MOLAuthenticatingURLSession")
git_override(
module_name = "molauthenticatingurlsession",
commit = "0a50a67f29d635a4012981714c1dedef9ac25fe6",
remote = "https://github.com/google/macops-molauthenticatingurlsession.git",
)
bazel_dep(name = "molcodesignchecker", version = "3.0", repo_name = "MOLCodesignChecker")
git_override(
module_name = "molcodesignchecker",
commit = "5060bcc8baa90bae3b0ca705d14850328bbbec53",
remote = "https://github.com/google/macops-molcodesignchecker.git",
)
bazel_dep(name = "molxpcconnection", version = "2.1", repo_name = "MOLXPCConnection")
git_override(
module_name = "molxpcconnection",
commit = "da816dc49becac96d941ef6a5c4153ed39d1fe7c",
remote = "https://github.com/russellhancox/macops-molxpcconnection.git",
)
non_module_deps = use_extension("//:non_module_deps.bzl", "non_module_deps")
use_repo(non_module_deps, "FMDB")
use_repo(non_module_deps, "OCMock")
bazel_dep(name = "hedron_compile_commands", dev_dependency = True)
git_override(
module_name = "hedron_compile_commands",
commit = "0e990032f3c5a866e72615cf67e5ce22186dcb97",
remote = "https://github.com/hedronvision/bazel-compile-commands-extractor.git",
)

View File

@@ -21,7 +21,7 @@ It is named Santa because it keeps track of binaries that are naughty or nice.
# Docs
The Santa docs are stored in the
[Docs](https://github.com/google/santa/blob/main/docs) directory and published
[Docs](https://github.com/google/santa/blob/main/docs) directory and are published
at https://santa.dev.
The docs include deployment options, details on how parts of Santa work and

View File

@@ -210,7 +210,7 @@ objc_library(
],
deps = [
":CertificateHelpers",
"@MOLCertificate",
":SNTStoredEvent",
],
)
@@ -478,13 +478,16 @@ santa_unit_test(
name = "SNTBlockMessageTest",
srcs = ["SNTBlockMessageTest.m"],
deps = [
":SNTBlockMessage",
":SNTBlockMessage_SantaGUI",
":SNTConfigurator",
":SNTFileAccessEvent",
":SNTStoredEvent",
":SNTSystemInfo",
"@OCMock",
],
sdk_frameworks = [
"AppKit",
],
)
santa_unit_test(

View File

@@ -20,6 +20,10 @@
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/SNTSystemInfo.h"
static id ValueOrNull(id value) {
return value ?: [NSNull null];
}
@implementation SNTBlockMessage
+ (NSAttributedString *)formatMessage:(NSString *)message {
@@ -52,7 +56,11 @@
#ifdef SANTAGUI
NSData *htmlData = [fullHTML dataUsingEncoding:NSUTF8StringEncoding];
return [[NSAttributedString alloc] initWithHTML:htmlData documentAttributes:NULL];
NSDictionary *options = @{
NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType,
NSCharacterEncodingDocumentAttribute : @(NSUTF8StringEncoding),
};
return [[NSAttributedString alloc] initWithHTML:htmlData options:options documentAttributes:NULL];
#else
NSString *strippedHTML = [self stringFromHTML:fullHTML];
if (!strippedHTML) {
@@ -123,35 +131,79 @@
}
+ (NSString *)replaceFormatString:(NSString *)str
withDict:(NSDictionary<NSString *, NSString * (^)()> *)replacements {
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];
}
}];
[replacements enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) {
if ((id)value != [NSNull null]) {
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:
//
// The following "format strings" will be replaced in the URL provided by
// `+eventDetailURLForEvent:customURL:templateMapping:`.
//
// %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.
// %file_bundle_id% - The bundle id of the binary, if any.
// %team_id% - The Team ID if present in the signature information.
// %signing_id% - The Signing ID if present in the signature information.
// %cdhash% - If signed, the CDHash.
// %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 {
+ (NSDictionary *)eventDetailTemplateMappingForEvent:(SNTStoredEvent *)event {
SNTConfigurator *config = [SNTConfigurator configurator];
return @{
@"%file_sha%" : ValueOrNull(event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil),
@"%file_identifier%" : ValueOrNull(event.fileSHA256),
@"%bundle_or_file_identifier%" :
ValueOrNull(event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil),
@"%username%" : ValueOrNull(event.executingUser),
@"%file_bundle_id%" : ValueOrNull(event.fileBundleID),
@"%team_id%" : ValueOrNull(event.teamID),
@"%signing_id%" : ValueOrNull(event.signingID),
@"%cdhash%" : ValueOrNull(event.cdhash),
@"%machine_id%" : ValueOrNull(config.machineID),
@"%hostname%" : ValueOrNull([SNTSystemInfo longHostname]),
@"%uuid%" : ValueOrNull([SNTSystemInfo hardwareUUID]),
@"%serial%" : ValueOrNull([SNTSystemInfo serialNumber]),
};
}
//
// Everything from `+eventDetailTemplateMappingForEvent:` with the following file access
// specific templates.
//
// %rule_version% - The version of the rule that was violated.
// %rule_name% - The name of the rule that was violated.
// %accessed_path% - The path accessed by the binary.
//
+ (NSDictionary *)fileAccessEventDetailTemplateMappingForEvent:(SNTFileAccessEvent *)event {
NSMutableDictionary *d = [self eventDetailTemplateMappingForEvent:event].mutableCopy;
[d addEntriesFromDictionary:@{
@"%rule_version%" : ValueOrNull(event.ruleVersion),
@"%rule_name%" : ValueOrNull(event.ruleName),
@"%accessed_path%" : ValueOrNull(event.accessedPath),
}];
return d;
}
// 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 "format strings" in `templateMapping` will be replaced in the URL, if they are present.
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event
customURL:(NSString *)url
templateMapping:(NSDictionary *)templateMapping {
SNTConfigurator *config = [SNTConfigurator configurator];
NSString *formatStr = url;
@@ -166,26 +218,7 @@
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];
formatStr = [SNTBlockMessage replaceFormatString:formatStr withDict:templateMapping];
NSURL *u = [NSURL URLWithString:formatStr];
if (!u) {
LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
@@ -194,55 +227,16 @@
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 *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url {
return [self eventDetailURLForEvent:event
customURL:url
templateMapping:[self eventDetailTemplateMappingForEvent:event]];
}
+ (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;
return [self eventDetailURLForEvent:event
customURL:url
templateMapping:[self fileAccessEventDetailTemplateMappingForEvent:event]];
}
@end

View File

@@ -39,18 +39,30 @@
OCMStub([self.mockSystemInfo serialNumber]).andReturn(@"my_s");
}
- (void)testFormatMessage {
NSString *input = @"Testing with somé Ünicode çharacters";
NSAttributedString *got = [SNTBlockMessage formatMessage:input];
XCTAssertEqualObjects([got string], input);
}
- (void)testEventDetailURLForEvent {
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
se.fileSHA256 = @"my_fi";
se.executingUser = @"my_un";
se.fileBundleID = @"s.n.t";
se.cdhash = @"abc";
se.teamID = @"SNT";
se.signingID = @"SNT:s.n.t";
NSString *url = @"http://"
@"localhost?fs=%file_sha%&fi=%file_identifier%&bfi=%bundle_or_file_identifier%&"
@"fbid=%file_bundle_id%&ti=%team_id%&si=%signing_id%&ch=%cdhash%&"
@"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";
NSString *wantUrl = @"http://"
@"localhost?fs=my_fi&fi=my_fi&bfi=my_fi&"
@"fbid=s.n.t&ti=SNT&si=SNT:s.n.t&ch=abc&"
@"un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
@@ -58,7 +70,9 @@
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";
@"localhost?fs=my_fbh&fi=my_fi&bfi=my_fbh&"
@"fbid=s.n.t&ti=SNT&si=SNT:s.n.t&ch=abc&"
@"un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
@@ -74,15 +88,22 @@
fae.ruleVersion = @"my_rv";
fae.ruleName = @"my_rn";
fae.fileSHA256 = @"my_fi";
fae.fileBundleID = @"s.n.t";
fae.cdhash = @"abc";
fae.teamID = @"SNT";
fae.signingID = @"SNT:s.n.t";
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 =
NSString *url =
@"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";
@"localhost?rv=%rule_version%&rn=%rule_name%&fi=%file_identifier%&"
@"fbid=%file_bundle_id%&ti=%team_id%&si=%signing_id%&ch=%cdhash%&"
@"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&"
@"fbid=s.n.t&ti=SNT&si=SNT:s.n.t&ch=abc&"
@"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];
@@ -92,4 +113,17 @@
XCTAssertNil([SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:@"null"]);
}
- (void)testEventDetailURLMissingDetails {
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
se.fileSHA256 = @"my_fi";
NSString *url = @"http://localhost?fi=%file_identifier%";
NSString *wantUrl = @"http://localhost?fi=my_fi";
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
XCTAssertEqualObjects(gotUrl.absoluteString, wantUrl);
}
@end

View File

@@ -182,6 +182,7 @@ typedef NS_ENUM(NSInteger, SNTRuleCleanup) {
};
#ifdef __cplusplus
enum class FileAccessPolicyDecision {
kNoPolicy,
kDenied,
@@ -190,6 +191,19 @@ enum class FileAccessPolicyDecision {
kAllowedReadAccess,
kAllowedAuditOnly,
};
enum class StatChangeStep {
kNoChange = 0,
kMessageCreate,
kCodesignValidation,
};
enum class StatResult {
kOK = 0,
kStatError,
kDevnoInodeMismatch,
};
#endif
static const char *kSantaDPath =

View File

@@ -14,12 +14,12 @@
#import <Foundation/Foundation.h>
#import <MOLCertificate/MOLCertificate.h>
#import "Source/common/SNTStoredEvent.h"
///
/// Represents an event stored in the database.
///
@interface SNTFileAccessEvent : NSObject <NSSecureCoding>
@interface SNTFileAccessEvent : SNTStoredEvent <NSSecureCoding>
///
/// The watched path that was accessed
@@ -32,57 +32,11 @@
@property NSString *ruleVersion;
@property NSString *ruleName;
///
/// The SHA256 of the process that accessed the path
///
@property NSString *fileSHA256;
///
/// The path of the process that accessed the watched path
///
@property NSString *filePath;
///
/// If the process is part of a bundle, the name of the application
///
@property NSString *application;
///
/// If the executed file was signed, this is the Team ID if present in the signature information.
///
@property NSString *teamID;
///
/// If the executed file was signed, this is the Signing ID if present in the signature information.
///
@property NSString *signingID;
///
/// The user who executed the binary.
///
@property NSString *executingUser;
///
/// The process ID of the binary being executed.
///
@property NSNumber *pid;
///
/// The parent process ID of the binary being executed.
///
@property NSNumber *ppid;
///
/// The name of the parent process.
///
@property NSString *parentName;
///
/// 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
///

View File

@@ -48,35 +48,20 @@
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
ENCODE(accessedPath);
ENCODE(ruleVersion);
ENCODE(ruleName);
ENCODE(fileSHA256);
ENCODE(filePath);
ENCODE(application);
ENCODE(teamID);
ENCODE(teamID);
ENCODE(pid);
ENCODE(ppid);
ENCODE(parentName);
ENCODE(signingChain);
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
self = [super init];
self = [super initWithCoder:decoder];
if (self) {
DECODE(accessedPath, NSString);
DECODE(ruleVersion, NSString);
DECODE(ruleName, NSString);
DECODE(fileSHA256, NSString);
DECODE(filePath, NSString);
DECODE(application, NSString);
DECODE(teamID, NSString);
DECODE(teamID, NSString);
DECODE(pid, NSNumber);
DECODE(ppid, NSNumber);
DECODE(parentName, NSString);
DECODEARRAY(signingChain, MOLCertificate);
}
return self;
}

View File

@@ -34,8 +34,7 @@ NSString *const kBundleID = @"com.google.santa.daemon";
#else
MOLCodesignChecker *cs = [[MOLCodesignChecker alloc] initWithSelf];
// "teamid.com.google.santa.daemon.xpc"
NSString *t = cs.signingInformation[@"teamid"];
return [NSString stringWithFormat:@"%@.%@.xpc", t, kBundleID];
return [NSString stringWithFormat:@"%@.%@.xpc", cs.teamID, kBundleID];
#endif
}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22689"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -18,18 +18,18 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Santa Blocked Execution" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" visibleAtLaunch="NO" animationBehavior="none" id="9Bq-yh-54f" customClass="SNTMessageWindow">
<windowStyleMask key="styleMask" utility="YES"/>
<window title="Santa Blocked Execution" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" visibleAtLaunch="NO" animationBehavior="none" titlebarAppearsTransparent="YES" titleVisibility="hidden" id="9Bq-yh-54f" customClass="SNTMessageWindow">
<windowStyleMask key="styleMask" titled="YES" utility="YES"/>
<rect key="contentRect" x="167" y="107" width="540" height="479"/>
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
<rect key="screenRect" x="0.0" y="0.0" width="1916" height="1099"/>
<view key="contentView" id="Iwq-Lx-rLv">
<rect key="frame" x="0.0" y="0.0" width="540" height="462"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kiB-jZ-69S">
<rect key="frame" x="16" y="434" width="37" height="32"/>
<rect key="frame" x="16" y="434" width="37" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Hidden Button" alternateTitle="This button exists so neither of the other two buttons is pre-selected when the dialog opens." bezelStyle="rounded" alignment="center" borderStyle="border" focusRingType="none" transparent="YES" imageScaling="proportionallyDown" inset="2" id="XGa-Sl-F4t">
<buttonCell key="cell" type="roundTextured" title="Hidden Button" alternateTitle="This button exists so neither of the other two buttons is pre-selected when the dialog opens." bezelStyle="texturedRounded" alignment="center" borderStyle="border" focusRingType="none" transparent="YES" imageScaling="proportionallyDown" inset="2" id="XGa-Sl-F4t">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<userDefinedRuntimeAttributes>
@@ -128,10 +128,6 @@
</textField>
<button toolTip="Show code signing certificate chain" translatesAutoresizingMaskIntoConstraints="NO" id="cJf-k6-OxS" userLabel="Publisher Certs Button">
<rect key="frame" x="62" y="235" width="15" height="15"/>
<constraints>
<constraint firstAttribute="width" constant="15" id="QTm-Iv-m5p"/>
<constraint firstAttribute="height" constant="15" id="YwG-0s-jop"/>
</constraints>
<buttonCell key="cell" type="bevel" bezelStyle="regularSquare" image="NSInfo" imagePosition="overlaps" alignment="center" refusesFirstResponder="YES" imageScaling="proportionallyDown" inset="2" id="R72-Qy-Xbb">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -139,6 +135,10 @@
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
</userDefinedRuntimeAttributes>
</buttonCell>
<constraints>
<constraint firstAttribute="width" constant="15" id="QTm-Iv-m5p"/>
<constraint firstAttribute="height" constant="15" id="YwG-0s-jop"/>
</constraints>
<connections>
<action selector="showCertInfo:" target="-2" id="dB0-a3-X31"/>
<binding destination="-2" name="hidden" keyPath="self.publisherInfo" id="fFR-f3-Oiw">
@@ -241,16 +241,17 @@
</textField>
<button verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="7ua-5a-uSd">
<rect key="frame" x="147" y="30" width="126" height="32"/>
<constraints>
<constraint firstAttribute="width" priority="900" constant="112" id="Pec-Pa-4aZ"/>
</constraints>
<buttonCell key="cell" type="push" title="Open..." bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</buttonCell>
<constraints>
<constraint firstAttribute="width" priority="900" constant="112" id="Pec-Pa-4aZ"/>
</constraints>
<connections>
<action selector="openEventDetails:" target="-2" id="VhL-ql-rCV"/>
<outlet property="nextKeyView" destination="BbV-3h-mmL" id="Xkz-va-iGc"/>
@@ -272,23 +273,19 @@ DQ
</textField>
<button translatesAutoresizingMaskIntoConstraints="NO" id="5D8-GP-a4l">
<rect key="frame" x="110" y="80" width="319" height="29"/>
<constraints>
<constraint firstAttribute="height" constant="25" id="KvD-X6-CsO"/>
</constraints>
<buttonCell key="cell" type="check" title="Prevent future notifications for this application for a day" bezelStyle="regularSquare" imagePosition="left" alignment="center" inset="2" id="R5Y-Uc-rEP">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="smallSystem"/>
</buttonCell>
<constraints>
<constraint firstAttribute="height" constant="25" id="KvD-X6-CsO"/>
</constraints>
<connections>
<binding destination="-2" name="value" keyPath="self.silenceFutureNotifications" id="tEb-2A-sht"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="BbV-3h-mmL" userLabel="Dismiss Button">
<rect key="frame" x="271" y="28" width="124" height="34"/>
<constraints>
<constraint firstAttribute="width" constant="110" id="6Uh-Bd-N64"/>
<constraint firstAttribute="height" constant="22" id="GH6-nw-6rD"/>
</constraints>
<buttonCell key="cell" type="push" title="Ignore" bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="XR6-Xa-gP4">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -296,6 +293,10 @@ DQ
Gw
</string>
</buttonCell>
<constraints>
<constraint firstAttribute="width" constant="110" id="6Uh-Bd-N64"/>
<constraint firstAttribute="height" constant="22" id="GH6-nw-6rD"/>
</constraints>
<accessibility description="Dismiss Dialog"/>
<connections>
<action selector="closeWindow:" target="-2" id="qQq-gh-8lw"/>

View File

@@ -92,6 +92,10 @@
NSString *eventDetailText = [[SNTConfigurator configurator] eventDetailText];
if (eventDetailText) {
[self.openEventButton setTitle:eventDetailText];
// Require the button keyEquivalent set to be CMD + Return
[self.openEventButton setKeyEquivalent:@"\r"]; // Return Key
[self.openEventButton
setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; // Command Key
}
}

View File

@@ -142,6 +142,8 @@ struct SNTFileAccessMessageWindowView: View {
Text(customText ?? "Open Event...").frame(maxWidth:.infinity)
})
.buttonStyle(.borderedProminent)
.keyboardShortcut(.defaultAction)
}
Button(action: dismissButton, label: {
Text("Dismiss").frame(maxWidth:.infinity)

View File

@@ -226,6 +226,15 @@
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
se.signingChain = cs.certificates;
se.cdhash = cs.cdhash;
se.teamID = cs.teamID;
if (cs.signingID) {
if (cs.teamID) {
se.signingID = [NSString stringWithFormat:@"%@:%@", cs.teamID, cs.signingID];
} else if (cs.platformBinary) {
se.signingID = [NSString stringWithFormat:@"platform:%@", cs.signingID];
}
}
dispatch_sync(dispatch_get_main_queue(), ^{
relatedEvents[se.fileSHA256] = se;

View File

@@ -380,23 +380,16 @@ REGISTER_COMMAND_NAME(@"fileinfo")
NSError *err;
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err];
NSString *cdhash =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoUnique];
NSString *teamID =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
NSString *identifier =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
NSString *cdhash = csc.cdhash;
NSString *teamID = csc.teamID;
NSString *identifier = csc.signingID;
NSString *signingID;
if (identifier) {
if (teamID) {
signingID = [NSString stringWithFormat:@"%@:%@", teamID, identifier];
} else {
id platformID =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
if ([platformID isKindOfClass:[NSNumber class]] && [platformID intValue] != 0) {
signingID = [NSString stringWithFormat:@"platform:%@", identifier];
}
} else if (csc.platformBinary) {
signingID = [NSString stringWithFormat:@"platform:%@", identifier];
}
}
@@ -522,21 +515,30 @@ REGISTER_COMMAND_NAME(@"fileinfo")
- (SNTAttributeBlock)teamID {
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
return [csc.signingInformation valueForKey:@"teamid"];
return csc.teamID;
};
}
- (SNTAttributeBlock)signingID {
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
NSString *identifier = csc.signingID;
NSString *teamID = csc.teamID;
if (!identifier) return nil;
if (teamID) {
return [NSString stringWithFormat:@"%@:%@", teamID, identifier];
} else if (csc.platformBinary) {
return [NSString stringWithFormat:@"platform:%@", identifier];
}
return nil;
};
}
- (SNTAttributeBlock)cdhash {
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoUnique];
return csc.cdhash;
};
}

View File

@@ -67,7 +67,7 @@ REGISTER_COMMAND_NAME(@"printlog")
- (void)runWithArguments:(NSArray *)arguments {
JsonPrintOptions options;
options.always_print_enums_as_ints = false;
options.always_print_primitive_fields = true;
options.always_print_fields_with_no_presence = true;
options.preserve_proto_field_names = true;
options.add_whitespace = true;

View File

@@ -61,7 +61,8 @@ REGISTER_COMMAND_NAME(@"rule")
@"\n"
@" One of:\n"
@" --path {path}: path of binary/bundle to add/remove.\n"
@" Will add the hash of the file currently at that path.\n"
@" Will add an appropriate rule for the file currently at that path.\n"
@" Defaults to a SHA-256 rule unless overridden with another flag.\n"
@" Does not work with --check. Use the fileinfo verb to check.\n"
@" the rule state of a file.\n"
@" --identifier {sha256|teamID|signingID|cdhash}: identifier to add/remove/check\n"
@@ -259,10 +260,17 @@ REGISTER_COMMAND_NAME(@"rule")
newRule.identifier = cs.leafCertificate.SHA256;
} else if (newRule.type == SNTRuleTypeCDHash) {
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
newRule.identifier =
[cs.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
} else if (newRule.type == SNTRuleTypeTeamID || newRule.type == SNTRuleTypeSigningID) {
// noop
newRule.identifier = cs.cdhash;
} else if (newRule.type == SNTRuleTypeTeamID) {
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
newRule.identifier = cs.teamID;
} else if (newRule.type == SNTRuleTypeSigningID) {
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
if (cs.teamID.length) {
newRule.identifier = [NSString stringWithFormat:@"%@:%@", cs.teamID, cs.signingID];
} else if (cs.platformBinary) {
newRule.identifier = [NSString stringWithFormat:@"platform:%@", cs.signingID];
}
}
}
@@ -291,7 +299,8 @@ REGISTER_COMMAND_NAME(@"rule")
if (newRule.state == SNTRuleStateUnknown) {
[self printErrorUsageAndExit:@"No state specified"];
} else if (!newRule.identifier) {
[self printErrorUsageAndExit:@"Either SHA-256, team ID, or path to file must be specified"];
[self printErrorUsageAndExit:
@"A valid SHA-256, CDHash, Signing ID, team ID, or path to file must be specified"];
}
[[self.daemonConn remoteObjectProxy]

View File

@@ -193,7 +193,7 @@ objc_library(
objc_library(
name = "SNTPolicyProcessor",
srcs = ["SNTPolicyProcessor.m"],
srcs = ["SNTPolicyProcessor.mm"],
hdrs = ["SNTPolicyProcessor.h"],
deps = [
":SNTRuleTable",
@@ -209,6 +209,19 @@ objc_library(
"@MOLCertificate",
"@MOLCodesignChecker",
"@MOLXPCConnection",
"@com_google_absl//absl/container:flat_hash_map",
],
)
santa_unit_test(
name = "SNTPolicyProcessorTest",
srcs = ["SNTPolicyProcessorTest.mm"],
deps = [
":SNTPolicyProcessor",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTConfigurator",
"//Source/common:SNTRule",
"//Source/common:TestUtils",
],
)
@@ -645,6 +658,7 @@ objc_library(
deps = [
":EndpointSecurityClient",
":WatchItemPolicy",
"//Source/common:SNTCommonEnums",
"//Source/santad/ProcessTree:process_tree",
],
)
@@ -705,6 +719,7 @@ objc_library(
srcs = ["Metrics.mm"],
hdrs = ["Metrics.h"],
deps = [
":EndpointSecurityMessage",
":SNTApplicationCoreMetrics",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTLogging",
@@ -779,6 +794,7 @@ objc_library(
"//Source/common:SNTXPCUnprivilegedControlInterface",
"//Source/common:Unit",
"//Source/santad/ProcessTree:process_tree",
"//Source/santad/ProcessTree/annotations:originator",
"@MOLXPCConnection",
],
)
@@ -912,9 +928,6 @@ santa_unit_test(
santa_unit_test(
name = "SantadTest",
srcs = ["SantadTest.mm"],
data = [
"//Source/santad/testdata:binaryrules_testdata",
],
minimum_os_version = "11.0",
sdk_dylibs = [
"bsm",
@@ -923,6 +936,9 @@ santa_unit_test(
sdk_frameworks = [
"DiskArbitration",
],
structured_resources = [
"//Source/santad/testdata:binaryrules_testdata",
],
tags = ["exclusive"],
deps = [
":EndpointSecurityMessage",
@@ -1014,7 +1030,7 @@ santa_unit_test(
santa_unit_test(
name = "EndpointSecuritySerializerProtobufTest",
srcs = ["Logs/EndpointSecurity/Serializers/ProtobufTest.mm"],
data = [
structured_resources = [
"//Source/santad/testdata:protobuf_json_testdata",
],
deps = [
@@ -1169,7 +1185,10 @@ santa_unit_test(
name = "MetricsTest",
srcs = ["MetricsTest.mm"],
deps = [
":EndpointSecurityMessage",
":Metrics",
":MockEndpointSecurityAPI",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTMetricSet",
"//Source/common:TestUtils",
"@OCMock",
@@ -1416,10 +1435,13 @@ test_suite(
":SNTEndpointSecurityTamperResistanceTest",
":SNTEventTableTest",
":SNTExecutionControllerTest",
":SNTPolicyProcessorTest",
":SNTRuleTableTest",
":SantadTest",
":WatchItemsTest",
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool_test",
"//Source/santad/ProcessTree:process_tree_test",
"//Source/santad/ProcessTree/annotations:originator_test",
],
visibility = ["//:santa_package_group"],
)

View File

@@ -152,13 +152,13 @@ void DARegisterDiskAppearedCallback(DASessionRef session, CFDictionaryRef __null
void DARegisterDiskDisappearedCallback(DASessionRef session, CFDictionaryRef __nullable match,
DADiskDisappearedCallback callback,
void *__nullable context) {};
void *__nullable context){};
void DARegisterDiskDescriptionChangedCallback(DASessionRef session,
CFDictionaryRef __nullable match,
CFArrayRef __nullable watch,
DADiskDescriptionChangedCallback callback,
void *__nullable context) {};
void *__nullable context){};
void DASessionSetDispatchQueue(DASessionRef session, dispatch_queue_t __nullable queue) {
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];

View File

@@ -20,11 +20,13 @@
#include <memory>
#include <string>
#import "Source/common/SNTCommonEnums.h"
#include "Source/santad/ProcessTree/process_tree.h"
namespace santa::santad::event_providers::endpoint_security {
class EndpointSecurityAPI;
class MessagePeer;
class Message {
public:
@@ -53,12 +55,22 @@ class Message {
std::string ParentProcessName() const;
void UpdateStatState(enum StatChangeStep step) const;
inline StatChangeStep StatChangeStep() const { return stat_change_step_; }
inline StatResult StatResult() const { return stat_result_; }
friend class santa::santad::event_providers::endpoint_security::MessagePeer;
private:
std::shared_ptr<EndpointSecurityAPI> esapi_;
const es_message_t* es_msg_;
std::optional<process_tree::ProcessToken> process_token_;
std::string GetProcessName(pid_t pid) const;
mutable enum StatChangeStep stat_change_step_ = StatChangeStep::kNoChange;
mutable enum StatResult stat_result_ = StatResult::kOK;
};
} // namespace santa::santad::event_providers::endpoint_security

View File

@@ -16,6 +16,7 @@
#include <bsm/libbsm.h>
#include <libproc.h>
#include <sys/stat.h>
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
@@ -24,6 +25,7 @@ namespace santa::santad::event_providers::endpoint_security {
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
: esapi_(std::move(esapi)), es_msg_(es_msg), process_token_(std::nullopt) {
esapi_->RetainMessage(es_msg);
UpdateStatState(StatChangeStep::kMessageCreate);
}
Message::~Message() {
@@ -38,6 +40,8 @@ Message::Message(Message &&other) {
other.es_msg_ = nullptr;
process_token_ = std::move(other.process_token_);
other.process_token_ = std::nullopt;
stat_change_step_ = other.stat_change_step_;
stat_result_ = other.stat_result_;
}
Message::Message(const Message &other) {
@@ -45,6 +49,27 @@ Message::Message(const Message &other) {
es_msg_ = other.es_msg_;
esapi_->RetainMessage(es_msg_);
process_token_ = other.process_token_;
stat_change_step_ = other.stat_change_step_;
stat_result_ = other.stat_result_;
}
void Message::UpdateStatState(enum StatChangeStep step) const {
// Only update state for AUTH EXEC events and if no previous change was detected
if (es_msg_->event_type == ES_EVENT_TYPE_AUTH_EXEC &&
stat_change_step_ == StatChangeStep::kNoChange &&
// Note: The following checks are required due to tests that only
// partially construct an es_message_t.
es_msg_->event.exec.target && es_msg_->event.exec.target->executable) {
struct stat &es_sb = es_msg_->event.exec.target->executable->stat;
struct stat sb;
int ret = stat(es_msg_->event.exec.target->executable->path.data, &sb);
// If stat failed, or if devno/inode changed, update state.
if (ret != 0 || es_sb.st_ino != sb.st_ino || es_sb.st_dev != sb.st_dev) {
stat_change_step_ = step;
// Determine the specific condition that failed for tracking purposes
stat_result_ = (ret != 0) ? StatResult::kStatError : StatResult::kDevnoInodeMismatch;
}
}
}
void Message::SetProcessToken(process_tree::ProcessToken tok) {

View File

@@ -92,7 +92,7 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
_notifyQueue = dispatch_queue_create(
"com.google.santa.daemon.notify_queue",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
QOS_CLASS_BACKGROUND, 0));
QOS_CLASS_UTILITY, 0));
}
return self;
}
@@ -146,12 +146,10 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
// sequence numbers are processed in order.
self->_metrics->UpdateEventStats(self->_processor, esMsg.operator->());
es_event_type_t eventType = esMsg->event_type;
if ([self handleContextMessage:esMsg]) {
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
self->_metrics->SetEventMetrics(self->_processor, eventType, EventDisposition::kProcessed,
processingEnd - processingStart);
self->_metrics->SetEventMetrics(self->_processor, EventDisposition::kProcessed,
processingEnd - processingStart, esMsg);
return;
}
@@ -159,13 +157,13 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
[self handleMessage:std::move(esMsg)
recordEventMetrics:^(EventDisposition disposition) {
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
self->_metrics->SetEventMetrics(self->_processor, eventType, disposition,
processingEnd - processingStart);
self->_metrics->SetEventMetrics(self->_processor, disposition,
processingEnd - processingStart, esMsg);
}];
} else {
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
self->_metrics->SetEventMetrics(self->_processor, eventType, EventDisposition::kDropped,
processingEnd - processingStart);
self->_metrics->SetEventMetrics(self->_processor, EventDisposition::kDropped,
processingEnd - processingStart, esMsg);
}
});

View File

@@ -387,16 +387,16 @@ class MockAuthResultCache : public AuthResultCache {
// Create mock disks with desired args
MockDADisk * (^CreateMockDisk)(NSString *, NSString *) =
^MockDADisk *(NSString *mountOn, NSString *mountFrom) {
MockDADisk *mockDisk = [[MockDADisk alloc] init];
mockDisk.diskDescription = @{
@"DAVolumePath" : mountOn, // f_mntonname,
@"DADevicePath" : mountOn, // f_mntonname,
@"DAMediaBSDName" : mountFrom, // f_mntfromname,
};
return mockDisk;
MockDADisk *mockDisk = [[MockDADisk alloc] init];
mockDisk.diskDescription = @{
@"DAVolumePath" : mountOn, // f_mntonname,
@"DADevicePath" : mountOn, // f_mntonname,
@"DAMediaBSDName" : mountFrom, // f_mntfromname,
};
return mockDisk;
};
// Reset the Mock DA property, setup disks and remount args, then trigger the test
void (^PerformStartupTest)(NSArray<MockDADisk *> *, NSArray<NSString *> *,
SNTDeviceManagerStartupPreferences) =

View File

@@ -381,7 +381,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, struct timespec event_time,
struct timespec processed_time) {
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
::pbv1::SantaMessage *santa_msg = Arena::Create<::pbv1::SantaMessage>(arena);
if (EnabledMachineID()) {
EncodeString([santa_msg] { return santa_msg->mutable_machine_id(); }, MachineID());
@@ -415,7 +415,7 @@ std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
// TODO: Profile this. It's probably not the most efficient way to do this.
JsonPrintOptions options;
options.always_print_enums_as_ints = false;
options.always_print_primitive_fields = true;
options.always_print_fields_with_no_presence = true;
options.preserve_proto_field_names = true;
std::string json;

View File

@@ -78,30 +78,21 @@ using santa::santad::logs::endpoint_security::serializers::GetModeEnum;
using santa::santad::logs::endpoint_security::serializers::GetPolicyDecision;
using santa::santad::logs::endpoint_security::serializers::GetReasonEnum;
@interface ProtobufTest : XCTestCase
@property id mockConfigurator;
@property id mockDecisionCache;
@property SNTCachedDecision *testCachedDecision;
@end
JsonPrintOptions DefaultJsonPrintOptions() {
JsonPrintOptions options;
options.always_print_enums_as_ints = false;
options.always_print_primitive_fields = false;
options.always_print_fields_with_no_presence = false;
options.preserve_proto_field_names = true;
options.add_whitespace = true;
return options;
}
NSString *TestJsonPath(NSString *jsonFileName, uint32_t version) {
static dispatch_once_t onceToken;
static NSString *testPath;
static NSString *testDataRepoPath = @"santa/Source/santad/testdata/protobuf";
NSString *testDataRepoVersionPath = [NSString stringWithFormat:@"v%u", version];
dispatch_once(&onceToken, ^{
testPath = [NSString pathWithComponents:@[
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testDataRepoPath
]];
});
return [NSString pathWithComponents:@[ testPath, testDataRepoVersionPath, jsonFileName ]];
}
NSString *EventTypeToFilename(es_event_type_t eventType) {
switch (eventType) {
case ES_EVENT_TYPE_NOTIFY_CLOSE: return @"close.json";
@@ -117,6 +108,16 @@ NSString *EventTypeToFilename(es_event_type_t eventType) {
}
}
NSString *TestJsonPath(NSString *jsonFileName, uint32_t version) {
NSString *p = [NSString pathWithComponents:@[
[[NSBundle bundleForClass:[ProtobufTest class]] resourcePath],
@"protobuf",
[NSString stringWithFormat:@"v%u", version],
jsonFileName,
]];
return p;
}
NSString *LoadTestJson(NSString *jsonFileName, uint32_t version) {
NSError *err = nil;
NSString *jsonData = [NSString stringWithContentsOfFile:TestJsonPath(jsonFileName, version)
@@ -325,12 +326,6 @@ void SerializeAndCheckNonESEvents(
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
@interface ProtobufTest : XCTestCase
@property id mockConfigurator;
@property id mockDecisionCache;
@property SNTCachedDecision *testCachedDecision;
@end
@implementation ProtobufTest
- (void)setUp {

View File

@@ -23,6 +23,7 @@
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool_platform_specific.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
namespace fsspool {

View File

@@ -26,6 +26,7 @@
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTMetricSet.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
namespace santa::santad {
@@ -54,6 +55,7 @@ enum class FileAccessMetricStatus {
using EventCountTuple = std::tuple<Processor, es_event_type_t, EventDisposition>;
using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
using EventStatsTuple = std::tuple<Processor, es_event_type_t>;
using EventStatChangeTuple = std::tuple<StatChangeStep, StatResult>;
using FileAccessMetricsPolicyVersion = std::string;
using FileAccessMetricsPolicyName = std::string;
using FileAccessEventCountTuple =
@@ -67,8 +69,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, SNTMetricCounter *drop_counts,
SNTMetricCounter *faa_event_counts, SNTMetricSet *metric_set,
void (^run_on_first_start)(Metrics *));
SNTMetricCounter *faa_event_counts, SNTMetricCounter *stat_change_counts,
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *));
~Metrics();
@@ -83,8 +85,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
// Used for tracking event sequence numbers to determine if drops occured
void UpdateEventStats(Processor processor, const es_message_t *msg);
void SetEventMetrics(Processor processor, es_event_type_t event_type,
EventDisposition disposition, int64_t nanos);
void SetEventMetrics(Processor processor, EventDisposition event_disposition, int64_t nanos,
const santa::santad::event_providers::endpoint_security::Message &msg);
void SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count);
@@ -112,6 +114,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
SNTMetricCounter *rate_limit_counts_;
SNTMetricCounter *faa_event_counts_;
SNTMetricCounter *drop_counts_;
SNTMetricCounter *stat_change_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
@@ -129,6 +132,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
std::map<Processor, int64_t> rate_limit_counts_cache_;
std::map<FileAccessEventCountTuple, int64_t> faa_event_counts_cache_;
std::map<EventStatsTuple, SequenceStats> drop_cache_;
std::map<EventStatChangeTuple, int64_t> stat_change_cache_;
};
} // namespace santa::santad

View File

@@ -13,6 +13,7 @@
/// limitations under the License.
#include "Source/santad/Metrics.h"
#include <EndpointSecurity/ESTypes.h>
#include <memory>
@@ -54,6 +55,14 @@ static NSString *const kPseudoEventTypeGlobal = @"Global";
static NSString *const kEventDispositionDropped = @"Dropped";
static NSString *const kEventDispositionProcessed = @"Processed";
static NSString *const kStatChangeStepNoChange = @"NoChange";
static NSString *const kStatChangeStepMessageCreate = @"MessageCreate";
static NSString *const kStatChangeStepCodesignValidation = @"CodesignValidation";
static NSString *const kStatResultOK = @"OK";
static NSString *const kStatResultStatError = @"StatError";
static NSString *const kStatResultDevnoInodeMismatch = @"DevnoInodeMismatch";
// Compat values
static NSString *const kFileAccessMetricStatusOK = @"OK";
static NSString *const kFileAccessMetricStatusBlockedUser = @"BLOCKED_USER";
@@ -148,6 +157,30 @@ NSString *const FileAccessPolicyDecisionToString(FileAccessPolicyDecision decisi
}
}
NSString *const StatChangeStepToString(StatChangeStep step) {
switch (step) {
case StatChangeStep::kNoChange: return kStatChangeStepNoChange;
case StatChangeStep::kMessageCreate: return kStatChangeStepMessageCreate;
case StatChangeStep::kCodesignValidation: return kStatChangeStepCodesignValidation;
default:
[NSException raise:@"Invalid stat change step"
format:@"Unknown stat change step value: %d", static_cast<int>(step)];
return nil;
}
}
NSString *const StatResultToString(StatResult result) {
switch (result) {
case StatResult::kOK: return kStatResultOK;
case StatResult::kStatError: return kStatResultStatError;
case StatResult::kDevnoInodeMismatch: return kStatResultDevnoInodeMismatch;
default:
[NSException raise:@"Invalid stat result"
format:@"Unknown stat result value: %d", static_cast<int>(result)];
return nil;
}
}
std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t interval) {
dispatch_queue_t q = dispatch_queue_create("com.google.santa.santametricsservice.q",
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
@@ -181,9 +214,14 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
fieldNames:@[ @"Processor", @"Event" ]
helpText:@"Count of the number of drops for each event"];
SNTMetricCounter *stat_change_counts =
[metric_set counterWithName:@"/santa/event_stat_change_count"
fieldNames:@[ @"step", @"error" ]
helpText:@"Count of times a stat info changed for a binary being evalauted"];
std::shared_ptr<Metrics> metrics = std::make_shared<Metrics>(
q, timer_source, interval, event_processing_times, event_counts, rate_limit_counts,
faa_event_counts, drop_counts, metric_set, ^(Metrics *metrics) {
faa_event_counts, drop_counts, stat_change_counts, metric_set, ^(Metrics *metrics) {
SNTRegisterCoreMetrics();
metrics->EstablishConnection();
});
@@ -204,8 +242,8 @@ 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, SNTMetricCounter *faa_event_counts,
SNTMetricCounter *drop_counts, SNTMetricSet *metric_set,
void (^run_on_first_start)(Metrics *))
SNTMetricCounter *drop_counts, SNTMetricCounter *stat_change_counts,
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *))
: q_(q),
timer_source_(timer_source),
interval_(interval),
@@ -214,6 +252,7 @@ Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t in
rate_limit_counts_(rate_limit_counts),
faa_event_counts_(faa_event_counts),
drop_counts_(drop_counts),
stat_change_counts_(stat_change_counts),
metric_set_(metric_set),
run_on_first_start_(run_on_first_start) {
SetInterval(interval_);
@@ -307,6 +346,15 @@ void Metrics::FlushMetrics() {
}
}
for (const auto &[key, count] : stat_change_cache_) {
if (count > 0) {
NSString *stepName = StatChangeStepToString(std::get<StatChangeStep>(key));
NSString *error = StatResultToString(std::get<StatResult>(key));
[stat_change_counts_ incrementBy:count forFieldValues:@[ stepName, error ]];
}
}
// Reset the maps so the next cycle begins with a clean state
// IMPORTANT: Do not reset drop_cache_, the sequence numbers must persist
// for accurate accounting
@@ -314,6 +362,7 @@ void Metrics::FlushMetrics() {
event_times_cache_ = {};
rate_limit_counts_cache_ = {};
faa_event_counts_cache_ = {};
stat_change_cache_ = {};
});
}
@@ -355,11 +404,17 @@ void Metrics::StopPoll() {
});
}
void Metrics::SetEventMetrics(Processor processor, es_event_type_t event_type,
EventDisposition event_disposition, int64_t nanos) {
void Metrics::SetEventMetrics(
Processor processor, EventDisposition event_disposition, int64_t nanos,
const santa::santad::event_providers::endpoint_security::Message &msg) {
dispatch_sync(events_q_, ^{
event_counts_cache_[EventCountTuple{processor, event_type, event_disposition}]++;
event_times_cache_[EventTimesTuple{processor, event_type}] = nanos;
event_counts_cache_[EventCountTuple{processor, msg->event_type, event_disposition}]++;
event_times_cache_[EventTimesTuple{processor, msg->event_type}] = nanos;
// Stat changes are only tracked for AUTH EXEC events
if (msg->event_type == ES_EVENT_TYPE_AUTH_EXEC) {
stat_change_cache_[EventStatChangeTuple{msg.StatChangeStep(), msg.StatResult()}]++;
}
});
}

View File

@@ -12,6 +12,8 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/Metrics.h"
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
@@ -19,13 +21,17 @@
#include <dispatch/dispatch.h>
#include <map>
#include <memory>
#import "Source/common/SNTCommonEnums.h"
#include "Source/common/SNTMetricSet.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/Metrics.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
using santa::santad::EventCountTuple;
using santa::santad::EventDisposition;
using santa::santad::EventStatChangeTuple;
using santa::santad::EventStatsTuple;
using santa::santad::EventTimesTuple;
using santa::santad::FileAccessEventCountTuple;
@@ -38,6 +44,8 @@ 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);
extern NSString *const StatChangeStepToString(StatChangeStep decision);
extern NSString *const StatResultToString(StatResult decision);
class MetricsPeer : public Metrics {
public:
@@ -55,12 +63,27 @@ class MetricsPeer : public Metrics {
using Metrics::interval_;
using Metrics::rate_limit_counts_cache_;
using Metrics::running_;
using Metrics::stat_change_cache_;
using Metrics::SequenceStats;
};
} // namespace santa::santad
namespace santa::santad::event_providers::endpoint_security {
class MessagePeer : public Message {
public:
// Make base class constructors visible
using Message::Message;
// Private member variables
using Message::stat_change_step_;
using Message::stat_result_;
};
} // namespace santa::santad::event_providers::endpoint_security
using santa::santad::EventDispositionToString;
using santa::santad::EventTypeToString;
using santa::santad::FileAccessMetricStatus;
@@ -69,10 +92,13 @@ using santa::santad::FileAccessPolicyDecisionToString;
using santa::santad::Metrics;
using santa::santad::MetricsPeer;
using santa::santad::ProcessorToString;
using santa::santad::StatChangeStepToString;
using santa::santad::StatResultToString;
using santa::santad::event_providers::endpoint_security::MessagePeer;
std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^block)(Metrics *)) {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
return std::make_shared<MetricsPeer>(q, timer, 100, nil, nil, nil, nil, nil, nil, block);
return std::make_shared<MetricsPeer>(q, timer, 100, nil, nil, nil, nil, nil, nil, nil, block);
}
@interface MetricsTest : XCTestCase
@@ -228,8 +254,44 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
}
}
- (void)testStatChangeStepToString {
std::map<StatChangeStep, NSString *> stepToString = {
{StatChangeStep::kNoChange, @"NoChange"},
{StatChangeStep::kMessageCreate, @"MessageCreate"},
{StatChangeStep::kCodesignValidation, @"CodesignValidation"},
};
for (const auto &kv : stepToString) {
XCTAssertEqualObjects(StatChangeStepToString(kv.first), kv.second);
}
XCTAssertThrows(StatChangeStepToString((StatChangeStep)12345));
}
- (void)testStatResultToString {
std::map<StatResult, NSString *> resultToString = {
{StatResult::kOK, @"OK"},
{StatResult::kStatError, @"StatError"},
{StatResult::kDevnoInodeMismatch, @"DevnoInodeMismatch"},
};
for (const auto &kv : resultToString) {
XCTAssertEqualObjects(StatResultToString(kv.first), kv.second);
}
XCTAssertThrows(StatResultToString((StatResult)12345));
}
- (void)testSetEventMetrics {
int64_t nanos = 1234;
es_message_t esExecMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, NULL);
es_message_t esOpenMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, NULL);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage();
MessagePeer execMsg(mockESApi, &esExecMsg);
MessagePeer openMsg(mockESApi, &esOpenMsg);
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
});
@@ -237,22 +299,27 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
// Initial maps are empty
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
XCTAssertEqual(metrics->stat_change_cache_.size(), 0);
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
EventDisposition::kProcessed, nanos);
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos, execMsg);
// Check sizes after setting metrics once
XCTAssertEqual(metrics->event_counts_cache_.size(), 1);
XCTAssertEqual(metrics->event_times_cache_.size(), 1);
XCTAssertEqual(metrics->stat_change_cache_.size(), 1);
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);
execMsg.stat_change_step_ = StatChangeStep::kMessageCreate;
execMsg.stat_result_ = StatResult::kDevnoInodeMismatch;
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos, execMsg);
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos * 2,
openMsg);
// Re-check expected counts. One was an update, so should only be 2 items
XCTAssertEqual(metrics->event_counts_cache_.size(), 2);
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
// Stat change counts should be 2 because one call wasn't an AUTH EXEC event
XCTAssertEqual(metrics->stat_change_cache_.size(), 2);
// Check map values
EventCountTuple ecExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
@@ -261,11 +328,16 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
EventDisposition::kProcessed};
EventTimesTuple etExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC};
EventTimesTuple etOpen{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN};
EventStatChangeTuple noChange{StatChangeStep::kNoChange, StatResult::kOK};
EventStatChangeTuple msgCreateChange{StatChangeStep::kMessageCreate,
StatResult::kDevnoInodeMismatch};
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);
XCTAssertEqual(metrics->stat_change_cache_[noChange], 1);
XCTAssertEqual(metrics->stat_change_cache_[msgCreateChange], 1);
}
- (void)testSetRateLimitingMetrics {
@@ -376,6 +448,14 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
id mockEventProcessingTimes = OCMClassMock([SNTMetricInt64Gauge class]);
id mockEventCounts = OCMClassMock([SNTMetricCounter class]);
int64_t nanos = 1234;
es_message_t esExecMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, NULL);
es_message_t esOpenMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, NULL);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage();
MessagePeer execMsg(mockESApi, &esExecMsg);
MessagePeer openMsg(mockESApi, &esOpenMsg);
// Initial update will have non-zero sequence numbers, triggering drop detection
es_message_t esMsgWithDrops = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, NULL);
@@ -395,17 +475,16 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
});
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, 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,
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->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos, execMsg);
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos * 2,
openMsg);
metrics->UpdateEventStats(Processor::kRecorder, &esMsgWithDrops);
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
@@ -416,6 +495,7 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 1);
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 1);
XCTAssertEqual(metrics->stat_change_cache_.size(), 1);
XCTAssertEqual(metrics->drop_cache_.size(), 2);
EventStatsTuple eventStats{Processor::kRecorder, esMsgWithDrops.event_type};
@@ -430,10 +510,11 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
// Expected call count is 8:
// 2: event counts
// 2: event times
// 1: stat change step
// 1: rate limit
// 1: FAA
// 2: drops (1 event, 1 global)
int expectedCalls = 8;
int expectedCalls = 9;
for (int i = 0; i < expectedCalls; i++) {
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush");
}
@@ -443,6 +524,7 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
XCTAssertEqual(metrics->stat_change_cache_.size(), 0);
// Note: The drop_cache_ should not be reset back to size 0. Instead, each
// entry has the sequence number left intact, but drop counts reset to 0.
XCTAssertEqual(metrics->drop_cache_.size(), 2);

View File

@@ -1,10 +1,13 @@
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
load("//:helper.bzl", "santa_unit_test")
package(
default_visibility = ["//:santa_package_group"],
)
cc_library(
name = "process",
hdrs = ["process.h"],
visibility = ["//:santa_package_group"],
deps = [
"//Source/santad/ProcessTree/annotations:annotator",
"@com_google_absl//absl/container:flat_hash_map",
@@ -26,7 +29,6 @@ objc_library(
sdk_dylibs = [
"bsm",
],
visibility = ["//:santa_package_group"],
deps = [
":process",
"//Source/santad/ProcessTree:process_tree_cc_proto",
@@ -42,12 +44,10 @@ objc_library(
proto_library(
name = "process_tree_proto",
srcs = ["process_tree.proto"],
visibility = ["//:santa_package_group"],
)
cc_proto_library(
name = "process_tree_cc_proto",
visibility = ["//:santa_package_group"],
deps = [":process_tree_proto"],
)
@@ -58,7 +58,6 @@ objc_library(
sdk_dylibs = [
"bsm",
],
visibility = ["//:santa_package_group"],
deps = [
":process_tree",
"//Source/santad:EndpointSecurityAPI",

View File

@@ -1,3 +1,5 @@
load("//:helper.bzl", "santa_unit_test")
package(
default_visibility = ["//:santa_package_group"],
)
@@ -9,3 +11,27 @@ cc_library(
"//Source/santad/ProcessTree:process_tree_cc_proto",
],
)
cc_library(
name = "originator",
srcs = ["originator.cc"],
hdrs = ["originator.h"],
deps = [
":annotator",
"//Source/santad/ProcessTree:process",
"//Source/santad/ProcessTree:process_tree",
"//Source/santad/ProcessTree:process_tree_cc_proto",
"@com_google_absl//absl/container:flat_hash_map",
],
)
santa_unit_test(
name = "originator_test",
srcs = ["originator_test.mm"],
deps = [
":originator",
"//Source/santad/ProcessTree:process",
"//Source/santad/ProcessTree:process_tree_cc_proto",
"//Source/santad/ProcessTree:process_tree_test_helpers",
],
)

View File

@@ -0,0 +1,67 @@
/// Copyright 2023 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/ProcessTree/annotations/originator.h"
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "Source/santad/ProcessTree/process.h"
#include "Source/santad/ProcessTree/process_tree.h"
#include "Source/santad/ProcessTree/process_tree.pb.h"
#include "absl/container/flat_hash_map.h"
namespace ptpb = ::santa::pb::v1::process_tree;
namespace santa::santad::process_tree {
void OriginatorAnnotator::AnnotateFork(ProcessTree &tree, const Process &parent,
const Process &child) {
// "Base case". Propagate existing annotations down to descendants.
if (auto annotation = tree.GetAnnotation<OriginatorAnnotator>(parent)) {
tree.AnnotateProcess(child, std::move(*annotation));
}
}
void OriginatorAnnotator::AnnotateExec(ProcessTree &tree,
const Process &orig_process,
const Process &new_process) {
static const absl::flat_hash_map<std::string, ptpb::Annotations::Originator>
originator_programs = {
{"/usr/bin/login",
ptpb::Annotations::Originator::Annotations_Originator_LOGIN},
{"/usr/sbin/cron",
ptpb::Annotations::Originator::Annotations_Originator_CRON},
};
if (auto annotation = tree.GetAnnotation<OriginatorAnnotator>(orig_process)) {
tree.AnnotateProcess(new_process, std::move(*annotation));
return;
}
if (auto it = originator_programs.find(new_process.program_->executable);
it != originator_programs.end()) {
tree.AnnotateProcess(new_process,
std::make_shared<OriginatorAnnotator>(it->second));
}
}
std::optional<ptpb::Annotations> OriginatorAnnotator::Proto() const {
auto annotation = ptpb::Annotations();
annotation.set_originator(originator_);
return annotation;
}
} // namespace santa::santad::process_tree

View File

@@ -0,0 +1,48 @@
/// Copyright 2023 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_ORIGINATOR_H
#define SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_ORIGINATOR_H
#include <optional>
#include "Source/santad/ProcessTree/annotations/annotator.h"
#include "Source/santad/ProcessTree/process.h"
#include "Source/santad/ProcessTree/process_tree.pb.h"
namespace santa::santad::process_tree {
class OriginatorAnnotator : public Annotator {
public:
OriginatorAnnotator()
: originator_(::santa::pb::v1::process_tree::Annotations::Originator::
Annotations_Originator_UNSPECIFIED){};
explicit OriginatorAnnotator(
::santa::pb::v1::process_tree::Annotations::Originator originator)
: originator_(originator){};
void AnnotateFork(ProcessTree &tree, const Process &parent,
const Process &child) override;
void AnnotateExec(ProcessTree &tree, const Process &orig_process,
const Process &new_process) override;
std::optional<::santa::pb::v1::process_tree::Annotations> Proto()
const override;
private:
::santa::pb::v1::process_tree::Annotations::Originator originator_;
};
} // namespace santa::santad::process_tree
#endif

View File

@@ -0,0 +1,78 @@
/// 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 <XCTest/XCTest.h>
#include "Source/santad/ProcessTree/annotations/originator.h"
#include "Source/santad/ProcessTree/process.h"
#include "Source/santad/ProcessTree/process_tree.pb.h"
#include "Source/santad/ProcessTree/process_tree_test_helpers.h"
using namespace santa::santad::process_tree;
namespace ptpb = ::santa::pb::v1::process_tree;
@interface OriginatorAnnotatorTest : XCTestCase
@property std::shared_ptr<ProcessTreeTestPeer> tree;
@property std::shared_ptr<const Process> initProc;
@end
@implementation OriginatorAnnotatorTest
- (void)setUp {
std::vector<std::unique_ptr<Annotator>> annotators;
annotators.emplace_back(std::make_unique<OriginatorAnnotator>());
self.tree = std::make_shared<ProcessTreeTestPeer>(std::move(annotators));
self.initProc = self.tree->InsertInit();
}
- (void)testAnnotation {
uint64_t event_id = 1;
const struct Cred cred = {.uid = 0, .gid = 0};
// PID 1.1: fork() -> PID 2.2
const struct Pid login_pid = {.pid = 2, .pidversion = 2};
self.tree->HandleFork(event_id++, *self.initProc, login_pid);
// PID 2.2: exec("/usr/bin/login") -> PID 2.3
const struct Pid login_exec_pid = {.pid = 2, .pidversion = 3};
const struct Program login_prog = {.executable = "/usr/bin/login", .arguments = {}};
auto login = *self.tree->Get(login_pid);
self.tree->HandleExec(event_id++, *login, login_exec_pid, login_prog, cred);
// Ensure we have an annotation on login itself...
login = *self.tree->Get(login_exec_pid);
auto annotation_opt = self.tree->GetAnnotation<OriginatorAnnotator>(*login);
XCTAssertTrue(annotation_opt.has_value());
auto proto_opt = (*annotation_opt)->Proto();
XCTAssertTrue(proto_opt.has_value());
XCTAssertEqual(proto_opt->originator(),
ptpb::Annotations::Originator::Annotations_Originator_LOGIN);
// PID 2.3: fork() -> PID 3.3
const struct Pid shell_pid = {.pid = 3, .pidversion = 3};
self.tree->HandleFork(event_id++, *login, shell_pid);
// PID 3.3: exec("/bin/zsh") -> PID 3.4
const struct Pid shell_exec_pid = {.pid = 3, .pidversion = 4};
const struct Program shell_prog = {.executable = "/bin/zsh", .arguments = {}};
auto shell = *self.tree->Get(shell_pid);
self.tree->HandleExec(event_id++, *shell, shell_exec_pid, shell_prog, cred);
// ... and also ensure we have the same annotation on the descendant zsh.
shell = *self.tree->Get(shell_exec_pid);
auto descendant_annotation_opt = self.tree->GetAnnotation<OriginatorAnnotator>(*shell);
XCTAssertTrue(descendant_annotation_opt.has_value());
XCTAssertEqual(*descendant_annotation_opt, *annotation_opt);
}
@end

View File

@@ -3,4 +3,11 @@ syntax = "proto3";
package santa.pb.v1.process_tree;
message Annotations {
enum Originator {
UNSPECIFIED = 0;
LOGIN = 1;
CRON = 2;
}
Originator originator = 1;
}

View File

@@ -256,9 +256,11 @@ static NSString *const kPrinterProxyPostMonterey =
// TODO(markowsky): Maybe add a metric here for how many large executables we're seeing.
// if (binInfo.fileSize > SomeUpperLimit) ...
SNTCachedDecision *cd = [self.policyProcessor
decisionForFileInfo:binInfo
targetProcess:targetProc
SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo
targetProcess:targetProc
preCodesignCheckCallback:^(void) {
esMsg.UpdateStatState(StatChangeStep::kCodesignValidation);
}
entitlementsFilterCallback:^NSDictionary *(const char *teamID, NSDictionary *entitlements) {
if (!entitlements) {
return nil;

View File

@@ -387,9 +387,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
}
- (void)testTeamIDAllowRule {
OCMStub([self.mockCodesignChecker signingInformation]).andReturn((@{
(__bridge NSString *)kSecCodeInfoTeamIdentifier : @(kExampleTeamID),
}));
OCMStub([self.mockCodesignChecker teamID]).andReturn(@(kExampleTeamID));
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateAllow;
@@ -405,9 +403,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
}
- (void)testTeamIDBlockRule {
OCMStub([self.mockCodesignChecker signingInformation]).andReturn((@{
(__bridge NSString *)kSecCodeInfoTeamIdentifier : @(kExampleTeamID),
}));
OCMStub([self.mockCodesignChecker teamID]).andReturn(@(kExampleTeamID));
SNTRule *rule = [[SNTRule alloc] init];
rule.state = SNTRuleStateBlock;

View File

@@ -17,6 +17,7 @@
#import <MOLCertificate/MOLCertificate.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTRule.h"
#import "Source/common/SNTRuleIdentifiers.h"
@class MOLCodesignChecker;
@@ -50,6 +51,13 @@
(NSDictionary *_Nullable (^_Nonnull)(
const char *_Nullable teamID,
NSDictionary *_Nullable entitlements))entitlementsFilterCallback;
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
targetProcess:(nonnull const es_process_t *)targetProc
preCodesignCheckCallback:(void (^_Nullable)(void))preCodesignCheckCallback
entitlementsFilterCallback:
(NSDictionary *_Nullable (^_Nonnull)(
const char *_Nullable teamID,
NSDictionary *_Nullable entitlements))entitlementsFilterCallback;
///
/// A wrapper for decisionForFileInfo:fileSHA256:certificateSHA256:. This method is slower as it
@@ -60,4 +68,12 @@
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
identifiers:(nonnull SNTRuleIdentifiers *)identifiers;
///
/// Updates a decision for a given file and agent configuration.
///
/// Returns YES if the decision requires no futher processing NO otherwise.
- (BOOL)decision:(nonnull SNTCachedDecision *)cd
forRule:(nonnull SNTRule *)rule
withTransitiveRules:(BOOL)transitive;
@end

View File

@@ -27,6 +27,7 @@
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTRule.h"
#import "Source/santad/DataLayer/SNTRuleTable.h"
#include "absl/container/flat_hash_map.h"
@interface SNTPolicyProcessor ()
@property SNTRuleTable *ruleTable;
@@ -44,37 +45,158 @@
return self;
}
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
cdhash:(nullable NSString *)cdhash
fileSHA256:(nullable NSString *)fileSHA256
certificateSHA256:(nullable NSString *)certificateSHA256
teamID:(nullable NSString *)teamID
signingID:(nullable NSString *)signingID
isProdSignedCallback:(BOOL (^_Nonnull)())isProdSignedCallback
entitlementsFilterCallback:
(NSDictionary *_Nullable (^_Nullable)(
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
cd.cdhash = cdhash;
cd.sha256 = fileSHA256 ?: fileInfo.SHA256;
cd.teamID = teamID;
cd.signingID = signingID;
// This method applies the rules to the cached decision object.
//
// It returns YES if the decision was made, NO if the decision was not made.
- (BOOL)decision:(SNTCachedDecision *)cd
forRule:(SNTRule *)rule
withTransitiveRules:(BOOL)enableTransitiveRules {
static const auto decisions =
absl::flat_hash_map<std::pair<SNTRuleType, SNTRuleState>, SNTEventState>{
{{SNTRuleTypeCDHash, SNTRuleStateAllow}, SNTEventStateAllowCDHash},
{{SNTRuleTypeCDHash, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler},
{{SNTRuleTypeCDHash, SNTRuleStateBlock}, SNTEventStateBlockCDHash},
{{SNTRuleTypeCDHash, SNTRuleStateSilentBlock}, SNTEventStateBlockCDHash},
{{SNTRuleTypeBinary, SNTRuleStateAllow}, SNTEventStateAllowBinary},
{{SNTRuleTypeBinary, SNTRuleStateAllowTransitive}, SNTEventStateAllowTransitive},
{{SNTRuleTypeBinary, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler},
{{SNTRuleTypeBinary, SNTRuleStateSilentBlock}, SNTEventStateBlockBinary},
{{SNTRuleTypeBinary, SNTRuleStateBlock}, SNTEventStateBlockBinary},
{{SNTRuleTypeSigningID, SNTRuleStateAllow}, SNTEventStateAllowSigningID},
{{SNTRuleTypeSigningID, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler},
{{SNTRuleTypeSigningID, SNTRuleStateSilentBlock}, SNTEventStateBlockSigningID},
{{SNTRuleTypeSigningID, SNTRuleStateBlock}, SNTEventStateBlockSigningID},
{{SNTRuleTypeCertificate, SNTRuleStateAllow}, SNTEventStateAllowCertificate},
{{SNTRuleTypeCertificate, SNTRuleStateSilentBlock}, SNTEventStateBlockCertificate},
{{SNTRuleTypeCertificate, SNTRuleStateBlock}, SNTEventStateBlockCertificate},
{{SNTRuleTypeTeamID, SNTRuleStateAllow}, SNTEventStateAllowTeamID},
{{SNTRuleTypeTeamID, SNTRuleStateSilentBlock}, SNTEventStateBlockTeamID},
{{SNTRuleTypeTeamID, SNTRuleStateBlock}, SNTEventStateBlockTeamID},
};
auto iterator = decisions.find(std::pair<SNTRuleType, SNTRuleState>{rule.type, rule.state});
if (iterator != decisions.end()) {
cd.decision = iterator->second;
} else {
// If we have an invalid state combination then either we have stale data in
// the database or a programming error. We treat this as if the
// corresponding rule was not found.
LOGE(@"Invalid rule type/state combination %ld/%ld", rule.type, rule.state);
return NO;
}
switch (rule.state) {
case SNTRuleStateSilentBlock: cd.silentBlock = YES; break;
case SNTRuleStateAllowCompiler:
if (!enableTransitiveRules) {
switch (rule.type) {
case SNTRuleTypeCDHash: cd.decision = SNTEventStateAllowCDHash; break;
case SNTRuleTypeBinary: cd.decision = SNTEventStateAllowBinary; break;
case SNTRuleTypeSigningID: cd.decision = SNTEventStateAllowSigningID; break;
default:
// Programming error. Something's marked as a compiler that shouldn't
// be.
LOGE(@"Invalid compiler rule type %ld", rule.type);
[NSException
raise:@"Invalid compiler rule type"
format:@"decision:forRule:withTransitiveRules: Unexpected compiler rule type: %ld",
rule.type];
break;
}
}
break;
case SNTRuleStateAllowTransitive:
// If transitive rules are disabled, then we treat
// SNTRuleStateAllowTransitive rules as if a matching rule was not found
// and set the state to unknown. Otherwise the decision map will have already set
// the EventState to SNTEventStateAllowTransitive.
if (!enableTransitiveRules) {
cd.decision = SNTEventStateUnknown;
return NO;
}
break;
default:
// If its not one of the special cases above, we don't need to do anything.
break;
}
// We know we have a match so apply the custom messages
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
return YES;
}
static void UpdateCachedDecisionSigningInfo(
SNTCachedDecision *cd, MOLCodesignChecker *csInfo,
NSDictionary *_Nullable (^entitlementsFilterCallback)(NSDictionary *_Nullable entitlements)) {
cd.certSHA256 = csInfo.leafCertificate.SHA256;
cd.certCommonName = csInfo.leafCertificate.commonName;
cd.certChain = csInfo.certificates;
// Check if we need to get teamID from code signing.
if (!cd.teamID) {
cd.teamID = csInfo.teamID;
}
// Ensure that if no teamID exists that the signing info confirms it is a
// platform binary. If not, remove the signingID.
if (!cd.teamID && cd.signingID) {
if (!csInfo.platformBinary) {
cd.signingID = nil;
}
}
NSDictionary *entitlements = csInfo.entitlements;
if (entitlementsFilterCallback) {
cd.entitlements = entitlementsFilterCallback(entitlements);
cd.entitlementsFiltered = (cd.entitlements.count != entitlements.count);
} else {
cd.entitlements = [entitlements sntDeepCopy];
cd.entitlementsFiltered = NO;
}
}
- (nonnull SNTCachedDecision *)
decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
cdhash:(nullable NSString *)cdhash
fileSHA256:(nullable NSString *)fileSHA256
certificateSHA256:(nullable NSString *)certificateSHA256
teamID:(nullable NSString *)teamID
signingID:(nullable NSString *)signingID
isProdSignedCallback:(BOOL (^_Nonnull)())isProdSignedCallback
entitlementsFilterCallback:(NSDictionary *_Nullable (^_Nullable)(
NSDictionary *_Nullable entitlements))entitlementsFilterCallback
preCodesignCheckCallback:(void (^_Nullable)(void))preCodesignCheckCallback {
// Check the hash before allocating a SNTCachedDecision.
NSString *fileHash = fileSHA256 ?: fileInfo.SHA256;
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];
SNTCachedDecision *systemCd = self.ruleTable.criticalSystemBinaries[fileHash];
if (systemCd) {
systemCd.decisionClientMode = mode;
return systemCd;
}
// Allocate a new cached decision for the execution.
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
cd.cdhash = cdhash;
cd.sha256 = fileHash;
cd.teamID = teamID;
cd.signingID = signingID;
cd.decisionClientMode = mode;
cd.quarantineURL = fileInfo.quarantineDataURL;
NSError *csInfoError;
if (certificateSHA256.length) {
cd.certSHA256 = certificateSHA256;
} else {
if (preCodesignCheckCallback) {
preCodesignCheckCallback();
}
// Grab the code signature, if there's an error don't try to capture
// any of the signature details. Also clear out any rule lookup parameters
// that would require being validly signed.
@@ -87,36 +209,9 @@
cd.signingID = nil;
cd.cdhash = nil;
} else {
cd.certSHA256 = csInfo.leafCertificate.SHA256;
cd.certCommonName = csInfo.leafCertificate.commonName;
cd.certChain = csInfo.certificates;
cd.teamID = teamID
?: [csInfo.signingInformation
objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
// Ensure that if no teamID exists that the signing info confirms it is a
// platform binary. If not, remove the signingID.
if (!cd.teamID && cd.signingID) {
id platformID = [csInfo.signingInformation
objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
if (![platformID isKindOfClass:[NSNumber class]] || [platformID intValue] == 0) {
cd.signingID = nil;
}
}
NSDictionary *entitlements =
csInfo.signingInformation[(__bridge NSString *)kSecCodeInfoEntitlementsDict];
if (entitlementsFilterCallback) {
cd.entitlements = entitlementsFilterCallback(entitlements);
cd.entitlementsFiltered = (cd.entitlements.count == entitlements.count);
} else {
cd.entitlements = [entitlements sntDeepCopy];
cd.entitlementsFiltered = NO;
}
UpdateCachedDecisionSigningInfo(cd, csInfo, entitlementsFilterCallback);
}
}
cd.quarantineURL = fileInfo.quarantineDataURL;
// Do not evaluate TeamID/SigningID rules for dev-signed code based on the
// assumption that orgs are generally more relaxed about dev signed cert
@@ -138,116 +233,11 @@
.certificateSHA256 = cd.certSHA256,
.teamID = cd.teamID}];
if (rule) {
switch (rule.type) {
case SNTRuleTypeCDHash:
switch (rule.state) {
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCDHash; return cd;
case SNTRuleStateAllowCompiler:
// If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules
// become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if
// it were SNTRuleStateAllowCDHash.
if ([self.configurator enableTransitiveRules]) {
cd.decision = SNTEventStateAllowCompiler;
} else {
cd.decision = SNTEventStateAllowCDHash;
}
return cd;
case SNTRuleStateSilentBlock:
cd.silentBlock = YES;
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockCDHash;
return cd;
default: break;
}
case SNTRuleTypeBinary:
switch (rule.state) {
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowBinary; return cd;
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 ([self.configurator enableTransitiveRules]) {
cd.decision = SNTEventStateAllowCompiler;
} else {
cd.decision = SNTEventStateAllowBinary;
}
return cd;
case SNTRuleStateAllowTransitive:
// If transitive rules are enabled, then SNTRuleStateAllowTransitive
// rules become SNTEventStateAllowTransitive decisions. Otherwise, we treat the
// rule as if it were SNTRuleStateUnknown.
if ([self.configurator enableTransitiveRules]) {
cd.decision = SNTEventStateAllowTransitive;
return cd;
} else {
rule.state = SNTRuleStateUnknown;
}
default: break;
}
break;
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;
}
break;
case SNTRuleTypeCertificate:
switch (rule.state) {
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCertificate; return cd;
case SNTRuleStateSilentBlock:
cd.silentBlock = YES;
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockCertificate;
return cd;
default: break;
}
break;
case SNTRuleTypeTeamID:
switch (rule.state) {
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowTeamID; return cd;
case SNTRuleStateSilentBlock:
cd.silentBlock = YES;
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockTeamID;
return cd;
default: break;
}
break;
default: break;
// If we have a rule match we don't need to process any further.
if ([self decision:cd
forRule:rule
withTransitiveRules:self.configurator.enableTransitiveRules]) {
return cd;
}
}
@@ -286,6 +276,19 @@
(NSDictionary *_Nullable (^_Nonnull)(
const char *_Nullable teamID,
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
return [self decisionForFileInfo:fileInfo
targetProcess:targetProc
preCodesignCheckCallback:nil
entitlementsFilterCallback:entitlementsFilterCallback];
}
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
targetProcess:(nonnull const es_process_t *)targetProc
preCodesignCheckCallback:(void (^_Nullable)(void))preCodesignCheckCallback
entitlementsFilterCallback:
(NSDictionary *_Nullable (^_Nonnull)(
const char *_Nullable teamID,
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
NSString *signingID;
NSString *teamID;
NSString *cdhash;
@@ -334,7 +337,8 @@
}
entitlementsFilterCallback:^NSDictionary *(NSDictionary *entitlements) {
return entitlementsFilterCallback(entitlementsFilterTeamID, entitlements);
}];
}
preCodesignCheckCallback:preCodesignCheckCallback];
}
// Used by `$ santactl fileinfo`.
@@ -371,7 +375,8 @@
return NO;
}
}
entitlementsFilterCallback:nil];
entitlementsFilterCallback:nil
preCodesignCheckCallback:nil];
}
///

View File

@@ -0,0 +1,564 @@
/// Copyright 2024 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <Foundation/Foundation.h>
#import "Source/santad/SNTPolicyProcessor.h"
#import <XCTest/XCTest.h>
#import "Source/common/SNTCachedDecision.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTRule.h"
#import "Source/santad/SNTPolicyProcessor.h"
@interface SNTPolicyProcessorTest : XCTestCase
@property SNTPolicyProcessor *processor;
@end
@implementation SNTPolicyProcessorTest
- (void)setUp {
self.processor = [[SNTPolicyProcessor alloc] init];
}
- (void)testRule:(SNTRule *)rule
transitiveRules:(BOOL)transitiveRules
final:(BOOL)final
matches:(BOOL)matches
silent:(BOOL)silent
expectedDecision:(SNTEventState)decision {
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
if (matches) {
switch (rule.type) {
case SNTRuleTypeBinary: cd.sha256 = rule.identifier; break;
case SNTRuleTypeCertificate: cd.certSHA256 = rule.identifier; break;
case SNTRuleTypeCDHash: cd.cdhash = rule.identifier; break;
default: break;
}
} else {
switch (rule.type) {
case SNTRuleTypeBinary:
cd.sha256 = @"2334567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
break;
case SNTRuleTypeCertificate:
cd.certSHA256 = @"2234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
break;
case SNTRuleTypeCDHash: cd.cdhash = @"b023fbe5361a5bbd793dc3889556e93f41ec9bb8"; break;
default: break;
}
}
BOOL decisionIsFinal = [self.processor decision:cd
forRule:rule
withTransitiveRules:transitiveRules];
XCTAssertEqual(cd.decision, decision);
XCTAssertEqual(decisionIsFinal, final);
XCTAssertEqual(cd.silentBlock, silent);
}
- (void)testDecisionForBlockByCDHashRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"CDHASH",
@"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8",
@"policy" : @"BLOCKLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateBlockCDHash];
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateBlockCDHash];
}
- (void)testDecisionForSilentBlockByCDHashRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"CDHASH",
@"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8",
@"policy" : @"SILENT_BLOCKLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:YES
expectedDecision:SNTEventStateBlockCDHash];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:YES
expectedDecision:SNTEventStateBlockCDHash];
}
- (void)testDecisionForAllowbyCDHashRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"CDHASH",
@"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8",
@"policy" : @"ALLOWLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowCDHash];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowCDHash];
}
- (void)testDecisionForBlockBySHA256RuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"BINARY",
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
@"policy" : @"BLOCKLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateBlockBinary];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateBlockBinary];
}
- (void)testDecisionForSilenBlockBySHA256RuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"BINARY",
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
@"policy" : @"SILENT_BLOCKLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:YES
expectedDecision:SNTEventStateBlockBinary];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:YES
expectedDecision:SNTEventStateBlockBinary];
}
- (void)testDecisionForAllowBySHA256RuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"BINARY",
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
@"policy" : @"ALLOWLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowBinary];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowBinary];
}
- (void)testDecisionForSigningIDBlockRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"SIGNINGID",
@"identifier" : @"ABCDEFGHIJ:ABCDEFGHIJ",
@"policy" : @"BLOCKLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateBlockSigningID];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateBlockSigningID];
}
// Signing ID rules
- (void)testDecisionForSigningIDSilentBlockRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"SIGNINGID",
@"identifier" : @"TEAMID1234:ABCDEFGHIJ",
@"policy" : @"SILENT_BLOCKLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:YES
expectedDecision:SNTEventStateBlockSigningID];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:YES
expectedDecision:SNTEventStateBlockSigningID];
}
- (void)testDecisionForSigningIDAllowRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"SIGNINGID",
@"identifier" : @"TEAMID1234:ABCDEFGHIJ",
@"policy" : @"ALLOWLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowSigningID];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowSigningID];
}
// Certificate rules
- (void)testDecisionForCertificateBlockRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"CERTIFICATE",
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
@"policy" : @"BLOCKLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateBlockCertificate];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateBlockCertificate];
}
- (void)testDecisionForCertificateSilentBlockRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"CERTIFICATE",
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
@"policy" : @"SILENT_BLOCKLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:YES
expectedDecision:SNTEventStateBlockCertificate];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:YES
expectedDecision:SNTEventStateBlockCertificate];
}
- (void)testDecisionForCertificateAllowRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"CERTIFICATE",
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
@"policy" : @"ALLOWLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowCertificate];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowCertificate];
}
// Team ID rules
- (void)testDecisionForTeamIDBlockRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"TEAMID",
@"identifier" : @"TEAMID1234",
@"policy" : @"BLOCKLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateBlockTeamID];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateBlockTeamID];
}
- (void)testDecisionForTeamIDSilentBlockRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"TEAMID",
@"identifier" : @"TEAMID1234",
@"policy" : @"SILENT_BLOCKLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:YES
expectedDecision:SNTEventStateBlockTeamID];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:YES
expectedDecision:SNTEventStateBlockTeamID];
}
- (void)testDecisionForTeamIDAllowRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"TEAMID",
@"identifier" : @"TEAMID1234",
@"policy" : @"ALLOWLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowTeamID];
// Ensure that nothing changes when disabling transitive rules.
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowTeamID];
}
// Compiler rules
// CDHash
- (void)testDecisionForCDHashCompilerRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"CDHASH",
@"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8",
@"policy" : @"ALLOWLIST_COMPILER"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowCompiler];
// Ensure disabling transitive rules results in a binary allow
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowCDHash];
}
// SHA256
- (void)testDecisionForSHA256CompilerRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"BINARY",
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
@"policy" : @"ALLOWLIST_COMPILER"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowCompiler];
// Ensure disabling transitive rules results in a binary allow
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowBinary];
}
// SigningID
- (void)testDecisionForSigningIDCompilerRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"SIGNINGID",
@"identifier" : @"TEAMID1234:ABCDEFGHIJ",
@"policy" : @"ALLOWLIST_COMPILER"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowCompiler];
// Ensure disabling transitive rules results in a Signing ID allow
[self testRule:rule
transitiveRules:NO
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowSigningID];
}
// Transitive allowlist rules
- (void)testDecisionForTransitiveAllowlistRuleMatches {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"BINARY",
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
@"policy" : @"ALLOWLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
rule.state = SNTRuleStateAllowTransitive;
[self testRule:rule
transitiveRules:YES
final:YES
matches:YES
silent:NO
expectedDecision:SNTEventStateAllowTransitive];
// Ensure that a transitive allowlist rule results in an
// SNTEventStateUnknown if transitive rules are disabled.
[self testRule:rule
transitiveRules:NO
final:NO
matches:YES
silent:NO
expectedDecision:SNTEventStateUnknown];
}
- (void)testEnsureANonMatchingRuleResultsInUnknown {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"BINARY",
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
@"policy" : @"ALLOWLIST"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
rule.state = static_cast<SNTRuleState>(88888); // Set to an invalid state
[self testRule:rule
transitiveRules:YES
final:NO
matches:NO
silent:NO
expectedDecision:SNTEventStateUnknown];
[self testRule:rule
transitiveRules:NO
final:NO
matches:YES
silent:NO
expectedDecision:SNTEventStateUnknown];
}
- (void)testEnsureCustomURLAndMessageAreSet {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
@"rule_type" : @"BINARY",
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
@"policy" : @"ALLOWLIST",
@"custom_msg" : @"Custom Message",
@"custom_url" : @"https://example.com"
}];
XCTAssertNotNil(rule, "invalid test rule dictionary");
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
cd.sha256 = rule.identifier;
[self.processor decision:cd forRule:rule withTransitiveRules:YES];
XCTAssertEqualObjects(cd.customMsg, @"Custom Message");
XCTAssertEqualObjects(cd.customURL, @"https://example.com");
}
@end

View File

@@ -24,6 +24,7 @@
#import "Source/santad/DataLayer/SNTRuleTable.h"
#include "Source/santad/DataLayer/WatchItems.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/ProcessTree/annotations/originator.h"
#include "Source/santad/ProcessTree/process_tree.h"
#import "Source/santad/SNTDatabaseController.h"
#include "Source/santad/SNTDecisionCache.h"
@@ -159,8 +160,11 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
std::vector<std::unique_ptr<process_tree::Annotator>> annotators;
for (NSString *annotation in [configurator enabledProcessAnnotations]) {
// TODO(nickmg): add annotation name switch
(void)annotation;
if ([[annotation lowercaseString] isEqualToString:@"originator"]) {
annotators.emplace_back(std::make_unique<process_tree::OriginatorAnnotator>());
} else {
LOGW(@"Unrecognized process annotation %@", annotation);
}
}
auto tree_status = process_tree::CreateTree(std::move(annotators));

View File

@@ -71,7 +71,6 @@ static void SetBinaryDataFromHexString(const char *hexStr, uint8_t *buf, size_t
}
}
static NSString *const testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
static const char *kAllowedSigningID = "com.google.allowed_signing_id";
static const char *kBlockedSigningID = "com.google.blocked_signing_id";
static const char *kNoRuleMatchSigningID = "com.google.no_rule_match_signing_id";
@@ -127,7 +126,8 @@ static const char *kBlockedCDHash = "7218eddfee4d3eba4873dedf22d1391d79aea25f";
OCMStub([mockConfigurator fileAccessPolicyUpdateIntervalSec]).andReturn(600);
NSString *testPath = [NSString pathWithComponents:@[
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testBinariesPath
[[NSBundle bundleForClass:[self class]] resourcePath],
@"binaryrules",
]];
OCMStub([self.mockSNTDatabaseController databasePath]).andReturn(testPath);

View File

@@ -1,5 +1,6 @@
#!/bin/bash
GIT_ROOT=$(git rev-parse --show-toplevel)
find $GIT_ROOT \( -name "*.m" -o -name "*.h" -o -name "*.mm" -o -name "*.cc" \) -exec xcrun clang-format -i {} \+
buildifier --lint=fix -r $GIT_ROOT
find ${GIT_ROOT} \( -name "*.m" -o -name "*.h" -o -name "*.mm" -o -name "*.cc" \) -exec xcrun clang-format -i {} \+
buildifier --lint=fix -r ${GIT_ROOT}
python3 -m pyink --config ${GIT_ROOT}/.pyink-config ${GIT_ROOT}

View File

@@ -1,6 +1,8 @@
load("//:helper.bzl", "run_command", "santa_unit_test")
package(default_visibility = ["//:santa_package_group"])
package(
default_visibility = ["//:santa_package_group"],
)
licenses(["notice"])

View File

@@ -40,8 +40,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@end
int main(int argc, const char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s bundle_path [usb_disk]", argv[0]);
if (argc < 3) {
fprintf(stderr, "Usage: %s bundle_path runner_disk [usb_disk]", argv[0]);
exit(-1);
}
@@ -50,14 +50,16 @@ int main(int argc, const char *argv[]) {
bundleDir = [bundleDir stringByAppendingString:@"/"];
}
NSString *usbDisk;
if (argc > 2) {
usbDisk = @(argv[2]);
NSString *runnerDisk = @(argv[2]);
NSString *usbDisk = NULL;
if (argc > 3) {
usbDisk = @(argv[3]);
}
VZVirtualMachine *vm =
[MacOSVirtualMachineConfigurationHelper createVirtualMachineWithBundleDir:bundleDir
roDisk:nil
roDisk:runnerDisk
usbDisk:usbDisk];
MacOSVirtualMachineDelegate *delegate = [MacOSVirtualMachineDelegate new];

View File

@@ -33,16 +33,23 @@ fi
# Install rosetta (for test binaries)
softwareupdate --install-rosetta --agree-to-license
# Install actions runner
mkdir ~/actions-runner
pushd ~/actions-runner
curl -o actions-runner-osx-arm64-2.296.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.296.0/actions-runner-osx-arm64-2.296.0.tar.gz
echo 'e358086b924d2e8d8abf50beec57ee7a3bb0c7d412f13abc51380f1b1894d776 actions-runner-osx-arm64-2.296.0.tar.gz' | shasum -a 256 -c
tar xzf ./actions-runner-osx-arm64-2.296.0.tar.gz
./config.sh --url https://github.com/google/santa
./svc.sh install
./svc.sh start
popd
# Add a LaunchAgent to start the mounted runner
tee ${HOME}/Library/LaunchAgents/runner.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.google.santa.e2erunner</string>
<key>ProgramArguments</key>
<array>
<string>/Volumes/init/run.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
EOF
# Run sample applescript to grant bash accessibility and automation control
clang "${SCRIPT_DIR}/disclaim.c" -o /tmp/disclaim

View File

@@ -1,121 +1,137 @@
#!/usr/bin/env python3
"""Download and run the given Santa E2E testing VM image."""
import datetime
import argparse
import json
import logging
import os
import pathlib
import shutil
import subprocess
import sys
import tempfile
import urllib.request
from google.cloud import storage
from google.oauth2 import service_account
PROJECT = "santa-e2e"
SA_KEY = "/opt/santa-e2e-sa.json"
BUCKET = "santa-e2e-vms"
COSIGN = "/opt/bin/cosign"
PUBKEY = "/opt/santa-e2e-vm-signer.pub"
VMCLI = "/opt/bin/VMCLI"
VMS_DIR = pathlib.Path.home() / "VMs"
TIMEOUT = 15 * 60 # in seconds
if __name__ == "__main__":
VMS_DIR.mkdir(exist_ok=True)
logging.basicConfig(level=logging.INFO)
tar_name = sys.argv[1]
if not tar_name.endswith(".tar.gz"):
print("Image name should be .tar.gz file", file=sys.stderr)
sys.exit(1)
parser = argparse.ArgumentParser(description="Start E2E VM")
# This is redundant, but kept to keep consistency with update_vm.py
parser.add_argument("--vm", help="VM tar.gz. name", required=True)
parser.add_argument(
"--vmcli", help="Path to VMCLI binary", default="/opt/bin/VMCLI"
)
args = parser.parse_args()
tar_path = VMS_DIR / tar_name
extracted_path = pathlib.Path(str(tar_path)[:-len(".tar.gz")])
if not args.vm.endswith(".tar.gz"):
logging.fatal("Image name should be .tar.gz file")
with open(SA_KEY, "rb") as key_file:
storage_client = storage.Client(
project=PROJECT,
credentials=service_account.Credentials.from_service_account_info(
json.load(key_file)),
)
bucket = storage_client.bucket(BUCKET)
blob = bucket.get_blob(tar_name)
if blob is None:
print("Specified image doesn't exist in GCS", file=sys.stderr)
sys.exit(1)
try:
local_ctime = os.stat(extracted_path).st_ctime
except FileNotFoundError:
local_ctime = 0
if blob.updated > datetime.datetime.fromtimestamp(
local_ctime, tz=datetime.timezone.utc):
print(f"VM {extracted_path} not present or not up to date, downloading...")
# Remove the old version of the image if present
try:
shutil.rmtree(extracted_path)
except FileNotFoundError:
pass
blob.download_to_filename(tar_path)
hash_blob = bucket.get_blob(str(tar_name) + ".sha256")
if hash_blob is None:
print("Image hash doesn't exist in GCS", file=sys.stderr)
sys.exit(1)
sig_blob = bucket.get_blob(str(tar_name) + ".sha256.sig")
if sig_blob is None:
print("Image signature doesn't exist in GCS", file=sys.stderr)
sys.exit(1)
hash_path = str(tar_path) + ".sha256"
hash_blob.download_to_filename(hash_path)
sig_path = str(tar_path) + ".sha256.sig"
sig_blob.download_to_filename(sig_path)
# cosign OOMs trying to sign/verify the tarball itself, so sign/verify
# the SHA256 of the tarball.
print("Verifying signature...")
# Verify the signature of the hash file is OK
subprocess.check_output([
COSIGN,
"verify-blob",
"--key", PUBKEY,
"--signature", sig_path,
hash_path,
])
# Then verify that the hash matches what we downloaded
subprocess.check_output(
["shasum", "-a", "256", "-c", hash_path],
cwd=VMS_DIR,
)
print("Extracting...")
subprocess.check_output(
["tar", "-C", VMS_DIR, "-x", "-S", "-z", "-f", tar_path]
)
tar_path.unlink()
tar_path = VMS_DIR / args.vm
extracted_path = pathlib.Path(str(tar_path)[: -len(".tar.gz")])
with tempfile.TemporaryDirectory() as snapshot_dir:
print(f"Snapshot: {snapshot_dir}")
logging.info(f"Snapshot: {snapshot_dir}")
# COW copy the image to this tempdir
subprocess.check_output(["cp", "-rc", extracted_path, snapshot_dir])
# Get a JIT runner key
github_token = os.environ["RUNNER_REG_TOKEN"]
body = json.dumps(
{
"name": os.environ["GITHUB_RUN_ID"] + " inner",
"runner_group_id": 1,
"labels": [
"self-hosted",
"macOS",
"ARM64",
"e2e-vm",
],
"work_folder": "/tmp/_work",
}
)
owner, repo = os.environ["GITHUB_REPOSITORY"].split("/", 1)
request = urllib.request.Request(
f"https://api.github.com/repos/{owner}/{repo}/actions/runners/generate-jitconfig",
headers={
"Content-Type": "application/json",
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {github_token}",
"X-GitHub-Api-Version": "2022-11-28",
},
data=body.encode("utf-8"),
)
with urllib.request.urlopen(request) as response:
jit_config = json.loads(response.read())["encoded_jit_config"]
logging.info("Got JIT runner config")
# Create a disk image to inject startup script
init_dmg = pathlib.Path(snapshot_dir) / "init.dmg"
subprocess.check_output(
[
"hdiutil",
"create",
"-attach",
"-size",
"1G",
"-fs",
"APFS",
"-volname",
"init",
init_dmg,
]
)
init_dmg_mount = pathlib.Path("/Volumes/init/")
# And populate startup script with runner and JIT key
with open(init_dmg_mount / "run.sh", "w") as run_sh:
run_sh.write(
f"""#!/bin/sh
set -xeuo pipefail
curl -L -o /tmp/runner.tar.gz 'https://github.com/actions/runner/releases/download/v2.316.0/actions-runner-osx-arm64-2.316.0.tar.gz'
echo "8442d39e3d91b67807703ec0825cec4384837b583305ea43a495a9867b7222ca /tmp/runner.tar.gz" | shasum -a 256 -c -
mkdir /tmp/runner
cd /tmp/runner
tar -xzf /tmp/runner.tar.gz
./run.sh --jitconfig '{jit_config}'
"""
)
os.chmod(init_dmg_mount / "run.sh", 0o755)
subprocess.check_output(["hdiutil", "detach", init_dmg_mount])
logging.info("Created init.dmg")
# Create a disk image for USB testing
usb_dmg = pathlib.Path(snapshot_dir) / "usb.dmg"
subprocess.check_output(["hdiutil", "create", "-size", "100M",
"-fs", "ExFAT", "-volname", "USB", usb_dmg])
subprocess.check_output(
[
"hdiutil",
"create",
"-size",
"100M",
"-fs",
"ExFAT",
"-volname",
"USB",
usb_dmg,
]
)
logging.info("Created usb.dmg")
try:
logging.info("Starting VM")
subprocess.check_output(
[VMCLI, pathlib.Path(snapshot_dir) / extracted_path.name, usb_dmg],
[
args.vmcli,
pathlib.Path(snapshot_dir) / extracted_path.name,
init_dmg,
usb_dmg,
],
timeout=TIMEOUT,
)
except subprocess.TimeoutExpired:
print("VM timed out")
print("VM deleted")
logging.warning("VM timed out")
logging.info("VM deleted")

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Download/update the given Santa E2E testing VM image."""
import argparse
import datetime
import logging
import os
import pathlib
import shutil
import subprocess
import sys
from google.cloud import storage
PROJECT = "santa-e2e"
BUCKET = "santa-e2e-vms"
COSIGN = "/opt/bin/cosign"
PUBKEY = "/opt/santa-e2e-vm-signer.pub"
VMS_DIR = pathlib.Path.home() / "VMs"
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
parser = argparse.ArgumentParser(description="Start E2E VM")
parser.add_argument("--vm", help="VM tar.gz. name", required=True)
args = parser.parse_args()
VMS_DIR.mkdir(exist_ok=True)
tar_name = args.vm
if not tar_name.endswith(".tar.gz"):
logging.fatal("Image name should be .tar.gz file")
tar_path = VMS_DIR / tar_name
extracted_path = pathlib.Path(str(tar_path)[: -len(".tar.gz")])
if "GOOGLE_APPLICATION_CREDENTIALS" not in os.environ:
logging.fatal("Missing GCS credentials file")
storage_client = storage.Client(project=PROJECT)
bucket = storage_client.bucket(BUCKET)
blob = bucket.get_blob(tar_name)
if blob is None:
logging.fatal("Specified image doesn't exist in GCS")
try:
local_ctime = os.stat(extracted_path).st_ctime
except FileNotFoundError:
local_ctime = 0
if blob.updated > datetime.datetime.fromtimestamp(
local_ctime, tz=datetime.timezone.utc
):
logging.info(
f"VM {extracted_path} not present or not up to date, downloading..."
)
# Remove the old version of the image if present
try:
shutil.rmtree(extracted_path)
except FileNotFoundError:
pass
blob.download_to_filename(tar_path)
hash_blob = bucket.get_blob(str(tar_name) + ".sha256")
if hash_blob is None:
logging.fatal("Image hash doesn't exist in GCS")
sig_blob = bucket.get_blob(str(tar_name) + ".sha256.sig")
if sig_blob is None:
logging.fatal("Image signature doesn't exist in GCS")
hash_path = str(tar_path) + ".sha256"
hash_blob.download_to_filename(hash_path)
sig_path = str(tar_path) + ".sha256.sig"
sig_blob.download_to_filename(sig_path)
# cosign OOMs trying to sign/verify the tarball itself, so sign/verify
# the SHA256 of the tarball.
logging.info("Verifying signature...")
# Verify the signature of the hash file is OK
subprocess.check_output(
[
COSIGN,
"verify-blob",
"--key",
PUBKEY,
"--signature",
sig_path,
hash_path,
]
)
# Then verify that the hash matches what we downloaded
subprocess.check_output(
["shasum", "-a", "256", "-c", hash_path],
cwd=VMS_DIR,
)
logging.info("Extracting...")
subprocess.check_output(
["tar", "-C", VMS_DIR, "-x", "-S", "-z", "-f", tar_path]
)
tar_path.unlink()

View File

@@ -1,14 +1,14 @@
#!/bin/bash
set -xo pipefail
set -exo pipefail
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 {} \+
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'
go install github.com/bazelbuild/buildtools/buildifier@latest
~/go/bin/buildifier --lint=warn -r $GIT_ROOT
~/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 {} \+
python3 -m pip install -q pyink
python3 -m pyink --config ${GIT_ROOT}/.pyink-config --check ${GIT_ROOT}

183
WORKSPACE
View File

@@ -3,166 +3,9 @@ workspace(name = "santa")
load(
"@bazel_tools//tools/build_defs/repo:git.bzl",
"git_repository",
"new_git_repository",
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# Abseil LTS branch, Aug 2023
http_archive(
name = "com_google_absl",
sha256 = "59d2976af9d6ecf001a81a35749a6e551a335b949d34918cfade07737b9d93c5",
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(
name = "rules_python",
sha256 = "48a838a6e1983e4884b26812b2c748a35ad284fd339eb8e2a6f3adf95307fbcd",
strip_prefix = "rules_python-0.16.2",
url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.16.2.tar.gz",
)
http_archive(
name = "build_bazel_rules_apple",
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_rules_swift//swift:repositories.bzl", "swift_rules_dependencies")
swift_rules_dependencies()
load(
"@build_bazel_rules_swift//swift:extras.bzl",
"swift_rules_extra_dependencies",
)
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
git_repository(
name = "hedron_compile_commands",
commit = "ac6411f8f347e5525038cb7858db4969db9e74f2",
remote = "https://github.com/hedronvision/bazel-compile-commands-extractor.git",
shallow_since = "1696885905 +0000",
)
load("@hedron_compile_commands//:workspace_setup.bzl", "hedron_compile_commands_setup")
hedron_compile_commands_setup()
# Googletest - tag: release-1.12.1
http_archive(
name = "com_google_googletest",
sha256 = "ab78fa3f912d44d38b785ec011a25f26512aaedc5291f51f3807c592b506d33a",
strip_prefix = "googletest-58d77fa8070e8cec2dc1ed015d66b454c8d78850",
urls = ["https://github.com/google/googletest/archive/58d77fa8070e8cec2dc1ed015d66b454c8d78850.zip"],
)
# 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")
protobuf_deps()
# Macops MOL* dependencies
git_repository(
name = "MOLAuthenticatingURLSession",
commit = "38b5ee46edb262481b16f950266a11d8cb77127c", # tag = v3.1
remote = "https://github.com/google/macops-molauthenticatingurlsession.git",
shallow_since = "1671479898 -0500",
)
git_repository(
name = "MOLCertificate",
commit = "288553b8ac75d7dd68159ef5b57652a506b8217c", # tag = "v2.1",
remote = "https://github.com/google/macops-molcertificate.git",
shallow_since = "1561303966 -0400",
)
git_repository(
name = "MOLCodesignChecker",
commit = "7ef66f1df15997defd7651b0ea5d6d9ec65a5b4f", # tag = "v2.2",
remote = "https://github.com/google/macops-molcodesignchecker.git",
shallow_since = "1561303990 -0400",
)
git_repository(
name = "MOLXPCConnection",
commit = "2c67c925c2b57fea9af551295d2b6711b38bb224", # tag = v2.1
remote = "https://github.com/google/macops-molxpcconnection.git",
shallow_since = "1564684202 -0400",
)
# FMDB
new_git_repository(
name = "FMDB",
build_file_content = """
objc_library(
name = "FMDB",
srcs = glob(["src/fmdb/*.m"], exclude=["src/fmdb.m"]),
hdrs = glob(["src/fmdb/*.h"]),
includes = ["src"],
sdk_dylibs = ["sqlite3"],
visibility = ["//visibility:public"],
)
""",
commit = "61e51fde7f7aab6554f30ab061cc588b28a97d04", # tag = 2.7.7
remote = "https://github.com/ccgus/fmdb.git",
shallow_since = "1589301502 -0700",
)
# OCMock
new_git_repository(
name = "OCMock",
build_file_content = """
objc_library(
name = "OCMock",
testonly = 1,
hdrs = glob(["Source/OCMock/*.h"]),
copts = [
"-Wno-vla",
],
includes = [
"Source",
"Source/OCMock",
],
non_arc_srcs = glob(["Source/OCMock/*.m"]),
pch = "Source/OCMock/OCMock-Prefix.pch",
visibility = ["//visibility:public"],
)
""",
commit = "afd2c6924e8a36cb872bc475248b978f743c6050", # tag = v3.9.1
patch_args = ["-p1"],
patches = ["//external_patches/OCMock:503.patch"],
remote = "https://github.com/erikdoe/ocmock",
shallow_since = "1635703064 +0100",
)
# Moroz (for testing)
http_archive(
@@ -198,29 +41,3 @@ go_rules_dependencies()
go_register_toolchains(version = "1.19.3")
gazelle_dependencies()
# Fuzzing
# rules_fuzzing requires an older python for now
http_archive(
name = "rules_python_fuzz",
sha256 = "c03246c11efd49266e8e41e12931090b613e12a59e6f55ba2efd29a7cb8b4258",
strip_prefix = "rules_python-0.11.0",
url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.11.0.tar.gz",
)
git_repository(
name = "rules_fuzzing",
commit = "b193df79b10dbfb4c623bda23e825e835f12bada", # Commit post PR 213 which fixes macOS
remote = "https://github.com/bazelbuild/rules_fuzzing",
repo_mapping = {"@rules_python": "@rules_python_fuzz"},
shallow_since = "1668184479 -0500",
)
load("@rules_fuzzing//fuzzing:repositories.bzl", "rules_fuzzing_dependencies")
rules_fuzzing_dependencies()
load("@rules_fuzzing//fuzzing:init.bzl", "rules_fuzzing_init")
rules_fuzzing_init()

View File

@@ -5,7 +5,7 @@ nav_order: 1
# Welcome to the Santa documentation
Santa is a binary and file access authorization system for macOS. It consists of a system extension that allows or denies attempted executions using a set of rules stored in a local database, a GUI agent that notifies the user in case of a block decision, a sync daemon responsible for syncing the database and a server, and a command-line utility for managing the system.
Santa is a binary and file access authorization system for macOS. It consists of a system extension that allows or denies attempted executions using a set of rules stored in a local database, a GUI agent that notifies the user in case of a block decision, a sync daemon responsible for syncing the database, and a server, and a command-line utility for managing the system.
It is named Santa because it keeps track of binaries that are naughty or nice.
@@ -13,19 +13,19 @@ The project and the latest release is available on [**GitHub**](https://github.c
## Features
* [**Multiple modes:**](concepts/mode.md) In the default `MONITOR` mode, all binaries except those marked as blocked will be allowed to run, whilst being logged and recorded in the events database. In `LOCKDOWN` mode, only listed binaries are allowed to run.
* [**Multiple modes:**](concepts/mode.md) In the default `MONITOR` mode, all binaries except those marked as blocked will be allowed to run, while logged and recorded in the events database. In `LOCKDOWN` mode, only listed binaries are allowed to run.
* [**Event logging:**](concepts/events.md) All binary launches are logged. When in either mode, all unknown or denied binaries are stored in the database to enable later aggregation.
* [**Several supported rule types:**](concepts/rules.md) Executions can be allowed or denied by specifying rules based on several attributes. The supported rule types, in order of highest to lowest precedence are: CDHash, binary hash, Signing ID, certificate hash, or Team ID. Since multiple rules can apply to a given binary, Santa will apply the rule with the highest precedence (i.e. you could use a Team ID rule to allow all binaries from some organization, but also add a Signing ID rule to deny a specific binary). Rules based on code signature properties (Signing ID, certificate hash, and Team ID) only apply if a bianry's signature validates correctly.
* [**Several supported rule types:**](concepts/rules.md) Executions can be allowed or denied by specifying rules based on several attributes. The supported rule types, in order of highest to lowest precedence are: CDHash, binary hash, Signing ID, certificate hash, or Team ID. Since multiple rules can apply to a given binary, Santa will apply the rule with the highest precedence (i.e. you could use a Team ID rule to allow all binaries from some organization, but also add a Signing ID rule to deny a specific binary). Rules based on code signature properties (Signing ID, certificate hash, and Team ID) only apply if a binary's signature validates correctly.
* **Path-based rules (via NSRegularExpression/ICU):** Binaries can be allowed/blocked based on the path they are launched from by matching against a configurable regex.
* [**Failsafe cert rules:**](concepts/rules.md#built-in-rules) You cannot put in a deny rule that would block the certificate used to sign launchd, a.k.a. pid 1, and therefore all components used in macOS. The binaries in every OS update (and in some cases entire new versions) are therefore automatically allowed. This does not affect binaries from Apple's App Store, which use various certs that change regularly for common apps. Likewise, you cannot block Santa itself.
* [**Failsafe cert rules:**](concepts/rules.md#built-in-rules) You cannot put in a deny rule that would block the certificate used to sign launchd, a.k.a. pid 1, and therefore all components used in macOS. The binaries in every OS update (and in some cases entire new versions) are therefore automatically allowed. This does not affect binaries from Apple's App Store, which uses various certs that change regularly for common apps. Likewise, you cannot block Santa itself.
* [**Components validate each other:**](binaries/index.md) Each of the components (the daemons, the GUI agent, and the command-line utility) communicate with each other using XPC and check that their signing certificates are identical before any communication is accepted.
* **Caching:** Allowed binaries are cached so the processing required to make a request is only done if the binary isn't already cached.
* **Caching:** Allowed binaries are cached, so the processing required to make a request is only done if the binary hasn't already been cached.
## Documentation overview
### Introduction
The following pages give an overview of how Santa accomplishes authorization at enterprise scale.
The following pages provide an overview of how Santa accomplishes authorization at an enterprise scale.
* [Binary Authorization](introduction/binary-authorization-overview.md): How Santa makes allow or deny decisions for any execution taking place.
* [Syncing](introduction/syncing-overview.md): How configuration and rules are applied from a sync server.

View File

@@ -20,7 +20,7 @@ NOTE: Synchronization is now performed by its own agent, the `santasyncservice`.
The phases of synchronization described below still apply, but references to how
the process starts is outdated. This will be updated soon.
This is a high level overview of the syncing process. For a more a more detailed
This is a high level overview of the syncing process. For a more detailed
account of each part, see the respective documentation. The santaclt binary can
be run in one of two modes, daemon and non-daemon. The non-daemon mode does one
full sync and exits. This is the typical way a user will interact with Santa,
@@ -29,8 +29,8 @@ syncs, listen for push notifications and upload events.
1. When the santad process starts up, it looks for a `SyncBaseURL` key/value in
the config. If one exists it will `fork()` and `execve()` `santactl sync
—-daemon`. Before the new process calls `execve()`, all privileges are
dropped. All privileged actions are then restricted to the XPC interface
—-daemon`. All privileges are dropped before the new process calls `execve()`.
All privileged actions are then restricted to the XPC interface
made available to santactl by santad. Since this santactl process is running
as a daemon it too exports an XPC interface so santad can interact with the
process efficiently and securely. To ensure syncing reliability santad will
@@ -51,8 +51,8 @@ syncs, listen for push notifications and upload events.
2. Firebase Cloud Messaging (FCM) can be used*. The sync server can send
down a configuration in the preflight to have the santactl daemon to
start listening for FCM messages. If a connection to FCM is made, the
full sync interval drops to a default of 4 hours. This can be further
configured by a preflight configuration. The FCM connection allows the
full sync interval drops to a default of 4 hours. A preflight configuration can override this.
The FCM connection allows the
sync-sever to talk directly with Santa. This way we can reduce polling
the sync server dramatically.
5. Full syncs will continue to take place at their configured interval. If
@@ -65,5 +65,5 @@ syncs, listen for push notifications and upload events.
When running as a daemon, the santactl process makes available an XPC interface
for use by santad. This allows santad to send blocked binary or bundle events
directly to santactl for immediate upload to the sync-server, enabling a
smoother user experience. The binary that was blocked on macOS is immediately
smoother user experience. The binary blocked on macOS is immediately
available for viewing or handling on the sync-server.

View File

@@ -5,11 +5,10 @@ nav_order: 7
## Known limitations
- Santa only blocks execution (execve and variants), it doesn't protect against dynamic libraries loaded with dlopen, libraries on disk that have been replaced, or libraries loaded using `DYLD_INSERT_LIBRARIES`.
- Santa only blocks execution (execve and variants); it doesn't protect against dynamic libraries loaded with dlopen, libraries on disk that have been replaced, or libraries loaded using `DYLD_INSERT_LIBRARIES`.
- Scripts: Santa is currently written to ignore any execution that isn't a binary. After weighing the administration cost versus the benefit, we found it wasn't worthwhile to manage the execution of scripts. Additionally, a number of applications make use of temporary generated scripts and blocking these could cause problems. We're happy to revisit this (or at least make it an option) if it would be useful to others.
- Scripts: Santa is written to ignore any execution that isn't a binary. After weighing the administrative cost versus the benefit, we found it wasn't worthwhile to manage the execution of scripts. Additionally, several applications make use of temporary scripts, and blocking these could cause problems. We're happy to revisit this (or at least make it an option) if it would be useful to others.
- USB Mass Storage Blocking: Santa's USB Mass Storage blocking feature is only meant to stop incidental
- USB Mass Storage Blocking: Santa's USB Mass Storage blocking feature only stops incidental
data exfiltration. It is not meant as a hard control. It cannot block:
* Storage devices mounted during boot prior to Santa having an opportunity to begin authorizing mounts
* Directly writing to an unmounted, but attached device

View File

@@ -1,15 +0,0 @@
diff --git a/protobuf_deps.bzl b/protobuf_deps.bzl
index ee552c13e..f0bba0385 100644
--- a/protobuf_deps.bzl
+++ b/protobuf_deps.bzl
@@ -70,8 +70,8 @@ def protobuf_deps():
_github_archive(
name = "utf8_range",
repo = "https://github.com/protocolbuffers/utf8_range",
- commit = "de0b4a8ff9b5d4c98108bdfe723291a33c52c54f",
- sha256 = "5da960e5e5d92394c809629a03af3c7709d2d3d0ca731dacb3a9fb4bf28f7702",
+ commit = "d863bc33e15cba6d873c878dcca9e6fe52b2f8cb",
+ sha256 = "568988b5f7261ca181468dba38849fabf59dd9200fb2ed4b2823da187ef84d8c",
)
if not native.existing_rule("rules_cc"):

View File

@@ -1,5 +0,0 @@
licenses(["notice"])
package(
default_visibility = ["//:santa_package_group"],
)

View File

@@ -21,12 +21,12 @@ def santa_unit_test(
name,
srcs = [],
deps = [],
data = [],
size = "medium",
minimum_os_version = "11.0",
resources = [],
structured_resources = [],
copts = [],
data = [],
**kwargs):
apple_resource_group(
name = "%s_resources" % name,
@@ -40,7 +40,7 @@ def santa_unit_test(
srcs = srcs,
deps = deps,
copts = copts,
data = [":%s_resources" % name],
data = data + [":%s_resources" % name],
**kwargs
)
@@ -50,6 +50,5 @@ def santa_unit_test(
minimum_os_version = minimum_os_version,
deps = [":%s_lib" % name],
size = size,
data = data,
visibility = ["//:__subpackages__"],
)

51
non_module_deps.bzl Normal file
View File

@@ -0,0 +1,51 @@
"""Modules for dependencies not included in the Bazel Central Registry"""
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
def _non_module_deps_impl(_):
# FMDB is used to access SQLite from Objective-C(++) code.
git_repository(
name = "FMDB",
remote = "https://github.com/ccgus/fmdb.git",
commit = "61e51fde7f7aab6554f30ab061cc588b28a97d04",
shallow_since = "1589301502 -0700",
build_file_content = """
objc_library(
name = "FMDB",
srcs = glob(["src/fmdb/*.m"], exclude=["src/fmdb.m"]),
hdrs = glob(["src/fmdb/*.h"]),
includes = ["src"],
sdk_dylibs = ["sqlite3"],
visibility = ["//visibility:public"],
)
""",
)
# OCMock is used in several tests.
git_repository(
name = "OCMock",
build_file_content = """
objc_library(
name = "OCMock",
testonly = 1,
hdrs = glob(["Source/OCMock/*.h"]),
copts = [
"-Wno-vla",
],
includes = [
"Source",
"Source/OCMock",
],
non_arc_srcs = glob(["Source/OCMock/*.m"]),
pch = "Source/OCMock/OCMock-Prefix.pch",
visibility = ["//visibility:public"],
)
""",
commit = "afd2c6924e8a36cb872bc475248b978f743c6050", # tag = v3.9.1
patch_args = ["-p1"],
patches = ["//external_patches/OCMock:503.patch"],
remote = "https://github.com/erikdoe/ocmock",
shallow_since = "1635703064 +0100",
)
non_module_deps = module_extension(implementation = _non_module_deps_impl)