mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcbbc33e5e | ||
|
|
ebe5166d77 | ||
|
|
6e5a530df5 | ||
|
|
1e88b88ee6 | ||
|
|
2d74f36ddb | ||
|
|
3a3564f36b | ||
|
|
d3c7cbbcc3 | ||
|
|
1ff6967934 | ||
|
|
53877f6114 | ||
|
|
8c50af4041 | ||
|
|
d0d4508f77 | ||
|
|
df3aac5baf | ||
|
|
e289056e5e | ||
|
|
4adad2ecfa | ||
|
|
dc1a3c27c2 | ||
|
|
a2f8030482 | ||
|
|
338a4f738f | ||
|
|
845d72eebd | ||
|
|
ca81270bff | ||
|
|
42cf1b232a | ||
|
|
57285c48dd | ||
|
|
2279cd8662 | ||
|
|
9423beecc8 | ||
|
|
b18d4a0e30 | ||
|
|
290ebed15e | ||
|
|
435868aa7a | ||
|
|
2e3952a31d | ||
|
|
60f53bc20a | ||
|
|
fec3766da4 | ||
|
|
ae63055f34 | ||
|
|
e5a0c3c1c0 | ||
|
|
5680c69164 | ||
|
|
8a978c1e75 | ||
|
|
6aa7c9ba86 | ||
|
|
6adef6a714 | ||
|
|
1d8c105257 | ||
|
|
e2d7cf04fc | ||
|
|
9d448071f7 | ||
|
|
cd6c0e7120 | ||
|
|
ec5e8177fb | ||
|
|
8e10c103cb | ||
|
|
db6c14ea10 | ||
|
|
4a4f1a971c | ||
|
|
c5c82a18ff | ||
|
|
f702c7a281 | ||
|
|
958ef52698 | ||
|
|
068ec885b2 | ||
|
|
e572f047c0 | ||
|
|
b904a329d9 | ||
|
|
d19343bccd | ||
|
|
09cd78d756 | ||
|
|
f169b69944 | ||
|
|
40f9872c54 | ||
|
|
5718f2e582 | ||
|
|
04fd742114 | ||
|
|
194a3a6d4a | ||
|
|
e1dc50fb36 | ||
|
|
9ff2f0d631 | ||
|
|
85058ec290 | ||
|
|
6e90673f71 | ||
|
|
a58cee908f | ||
|
|
80b26955b4 | ||
|
|
6a84023548 | ||
|
|
e70acefb5c |
@@ -1,5 +1,6 @@
|
||||
# Ignore reason: These crafted binaries are used in tests
|
||||
ignorePaths:
|
||||
- Fuzzing/common/MachOParse_corpus/ret0
|
||||
- Source/common/testdata/bad_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
|
||||
6
.bazelrc
6
.bazelrc
@@ -17,3 +17,9 @@ build:asan --copt="-fno-omit-frame-pointer"
|
||||
build:asan --copt="-fsanitize=address"
|
||||
build:asan --copt="-DADDRESS_SANITIZER"
|
||||
build:asan --linkopt="-fsanitize=address"
|
||||
|
||||
build:fuzz --copt="-Wno-macro-redefined"
|
||||
build:fuzz --copt="-D_FORTIFY_SOURCE=0"
|
||||
build:fuzz --@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing//fuzzing/engines:libfuzzer
|
||||
build:fuzz --@rules_fuzzing//fuzzing:cc_engine_instrumentation=libfuzzer
|
||||
build:fuzz --@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -24,18 +24,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-10.15, macos-11, macos-12]
|
||||
os: [macos-11, macos-12]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build Userspace
|
||||
run: bazel build --apple_generate_dsym -c opt :release --define=SANTA_BUILD_TYPE=adhoc
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build Userspace
|
||||
run: bazel build --apple_generate_dsym -c opt :release --define=SANTA_BUILD_TYPE=adhoc
|
||||
|
||||
unit_tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-10.15, macos-11, macos-12]
|
||||
os: [macos-11, macos-12]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
2
.github/workflows/continuous.yml
vendored
2
.github/workflows/continuous.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: continuous
|
||||
on:
|
||||
schedule:
|
||||
- cron: '* 10 * * *' # Every day at 10:00 UTC
|
||||
- cron: '0 10 * * *' # Every day at 10:00 UTC
|
||||
workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
|
||||
|
||||
jobs:
|
||||
|
||||
41
.github/workflows/e2e.yml
vendored
Normal file
41
.github/workflows/e2e.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: E2E
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
start_vm:
|
||||
runs-on: e2e-host
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Start VM
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_12.bundle.tar.gz
|
||||
|
||||
integration:
|
||||
runs-on: e2e-vm
|
||||
env:
|
||||
VM_PASSWORD: ${{ secrets.VM_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install configuration profile
|
||||
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
|
||||
- name: Add homebrew to PATH
|
||||
run: echo "/opt/homebrew/bin/" >> $GITHUB_PATH
|
||||
- name: Build, install, and start moroz
|
||||
run: |
|
||||
bazel build @com_github_groob_moroz//cmd/moroz:moroz
|
||||
cp bazel-bin/external/com_github_groob_moroz/cmd/moroz/moroz_/moroz /tmp/moroz
|
||||
/tmp/moroz -configs="$GITHUB_WORKSPACE/Testing/integration/configs/moroz_default/global.toml" -use-tls=false &
|
||||
- name: Build, install, and sync santa
|
||||
run: |
|
||||
bazel run :reload --define=SANTA_BUILD_TYPE=adhoc
|
||||
bazel run //Testing/integration:allow_sysex
|
||||
sudo santactl sync --debug
|
||||
- name: Run integration test binaries
|
||||
run: bazel test //Testing/integration:integration_tests
|
||||
- name: Test config changes
|
||||
run: ./Testing/integration/test_config_changes.sh
|
||||
- name: Test sync server changes
|
||||
run: ./Testing/integration/test_sync_changes.sh
|
||||
- name: Poweroff
|
||||
if: ${{ always() }}
|
||||
run: sudo shutdown -h +1
|
||||
35
.github/workflows/fuzz.yml
vendored
Normal file
35
.github/workflows/fuzz.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Fuzzing
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # Every day at 6:00 UTC
|
||||
workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
|
||||
|
||||
jobs:
|
||||
start_vm:
|
||||
runs-on: e2e-host
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Start VM
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_13.bundle.tar.gz
|
||||
|
||||
fuzz:
|
||||
runs-on: e2e-vm
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup libfuzzer
|
||||
run: Fuzzing/install_libclang_fuzzer.sh
|
||||
- name: Fuzz
|
||||
run: |
|
||||
for target in $(bazel query 'kind(fuzzing_launcher, //Fuzzing:all)'); do
|
||||
bazel run --config=fuzz $target -- -- -max_len=32768 -runs=1000000 -timeout=5
|
||||
done
|
||||
- name: Upload crashes
|
||||
uses: actions/upload-artifact@v1
|
||||
if: failure()
|
||||
with:
|
||||
name: artifacts
|
||||
path: /tmp/fuzzing/artifacts
|
||||
- name: Poweroff VM
|
||||
if: ${{ always() }}
|
||||
run: sudo shutdown -h +1
|
||||
429
.pylintrc
Normal file
429
.pylintrc
Normal file
@@ -0,0 +1,429 @@
|
||||
# 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
|
||||
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
* @google/macendpoints
|
||||
11
Fuzzing/BUILD
Normal file
11
Fuzzing/BUILD
Normal file
@@ -0,0 +1,11 @@
|
||||
load("fuzzing.bzl", "objc_fuzz_test")
|
||||
|
||||
objc_fuzz_test(
|
||||
name = "MachOParse",
|
||||
srcs = ["common/MachOParse.mm"],
|
||||
corpus = glob(["common/MachOParse_corpus/*"]),
|
||||
linkopts = ["-lsqlite3"],
|
||||
deps = [
|
||||
"//Source/common:SNTFileInfo",
|
||||
],
|
||||
)
|
||||
40
Fuzzing/common/MachOParse.mm
Normal file
40
Fuzzing/common/MachOParse.mm
Normal file
@@ -0,0 +1,40 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <libproc.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
|
||||
int get_num_fds() {
|
||||
return proc_pidinfo(getpid(), PROC_PIDLISTFDS, 0, NULL, 0) / PROC_PIDLISTFD_SIZE;
|
||||
}
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
static NSString *tmpPath =
|
||||
[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
|
||||
|
||||
int num_fds_pre = get_num_fds();
|
||||
|
||||
@autoreleasepool {
|
||||
NSData *input = [NSData dataWithBytesNoCopy:(void *)data length:size freeWhenDone:false];
|
||||
[input writeToFile:tmpPath atomically:false];
|
||||
|
||||
NSError *error;
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithResolvedPath:tmpPath error:&error];
|
||||
if (!fi || error != nil) {
|
||||
NSLog(@"Error: %@", error);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Mach-O Parsing
|
||||
[fi architectures];
|
||||
[fi isMissingPageZero];
|
||||
[fi infoPlist];
|
||||
}
|
||||
|
||||
if (num_fds_pre != get_num_fds()) {
|
||||
abort();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
BIN
Fuzzing/common/MachOParse_corpus/ret0
Normal file
BIN
Fuzzing/common/MachOParse_corpus/ret0
Normal file
Binary file not shown.
20
Fuzzing/fuzzing.bzl
Normal file
20
Fuzzing/fuzzing.bzl
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Utilities for fuzzing Santa"""
|
||||
|
||||
load("@rules_fuzzing//fuzzing:cc_defs.bzl", "cc_fuzz_test")
|
||||
|
||||
def objc_fuzz_test(name, srcs, deps, corpus, linkopts = [], **kwargs):
|
||||
native.objc_library(
|
||||
name = "%s_lib" % name,
|
||||
srcs = srcs,
|
||||
deps = deps,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
cc_fuzz_test(
|
||||
name = name,
|
||||
deps = [
|
||||
"%s_lib" % name,
|
||||
],
|
||||
linkopts = linkopts,
|
||||
corpus = corpus,
|
||||
)
|
||||
14
Fuzzing/install_libclang_fuzzer.sh
Executable file
14
Fuzzing/install_libclang_fuzzer.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Xcode doesn't include the fuzzer runtime, but the one LLVM ships is compatible with Apple clang.
|
||||
set -uexo pipefail
|
||||
|
||||
CLANG_VERSION=$(clang --version | head -n 1 | cut -d' ' -f 4)
|
||||
DST_PATH="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.fuzzer_osx.a"
|
||||
|
||||
if [ -f ${DST_PATH} ]; then
|
||||
exit 0;
|
||||
fi
|
||||
|
||||
curl -O -L https://github.com/llvm/llvm-project/releases/download/llvmorg-${CLANG_VERSION}/clang+llvm-${CLANG_VERSION}-x86_64-apple-darwin.tar.xz
|
||||
tar xvf clang+llvm-${CLANG_VERSION}-x86_64-apple-darwin.tar.xz clang+llvm-${CLANG_VERSION}-x86_64-apple-darwin/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.fuzzer_osx.a
|
||||
cp clang+llvm-${CLANG_VERSION}-x86_64-apple-darwin/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.fuzzer_osx.a ${DST_PATH}
|
||||
4
Fuzzing/libFuzzer/.gitignore
vendored
4
Fuzzing/libFuzzer/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
bin
|
||||
llvm-*.src
|
||||
llvm-*.src.tar.xz
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
LLVM_VERSION='5.0.1'
|
||||
LLVM_COMPILERRT_TARBALL_NAME="llvm-${LLVM_VERSION}.src.tar.xz"
|
||||
LLVM_COMPILERRT_SRC_FOLDER_NAME=`echo "${LLVM_COMPILERRT_TARBALL_NAME}" | cut -d '.' -f 1-4`
|
||||
LLVM_COMPILERRT_TARBALL_URL="http://releases.llvm.org/${LLVM_VERSION}/${LLVM_COMPILERRT_TARBALL_NAME}"
|
||||
|
||||
LIBFUZZER_FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
LOG_FILE=`mktemp`
|
||||
|
||||
main() {
|
||||
echo "libFuzzer build script"
|
||||
|
||||
echo " > Checking dependencies..."
|
||||
checkDependencies || return 1
|
||||
|
||||
echo " > Entering libFuzzer folder..."
|
||||
cd "${LIBFUZZER_FOLDER}" > /dev/null 2>&1
|
||||
if [ $? -ne 0 ] ; then
|
||||
echo "Failed to enter the libFuzzer folder: ${LIBFUZZER_FOLDER}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${LLVM_COMPILERRT_TARBALL_NAME}" ] ; then
|
||||
echo " > Downloading the LLVM tarball..."
|
||||
curl "${LLVM_COMPILERRT_TARBALL_URL}" -o "${LLVM_COMPILERRT_TARBALL_NAME}" > "${LOG_FILE}" 2>&1
|
||||
if [ $? -ne 0 ] ; then
|
||||
dumpLogFile "Failed to download the LLVM tarball"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo " > An existing LLVM tarball was found"
|
||||
fi
|
||||
|
||||
if [ -d "${LLVM_COMPILERRT_SRC_FOLDER_NAME}" ] ; then
|
||||
echo " > Deleting existing LLVM folder..."
|
||||
rm -rf "${LLVM_COMPILERRT_SRC_FOLDER_NAME}" > "${LOG_FILE}" 2>&1
|
||||
if [ $? -ne 0 ] ; then
|
||||
dumpLogFile "Failed to delete the existing source folder"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo " > Extracting the LLVM tarball..."
|
||||
tar xf "${LLVM_COMPILERRT_TARBALL_NAME}" > "${LOG_FILE}" 2>&1
|
||||
if [ $? -ne 0 ] ; then
|
||||
rm "${LLVM_COMPILERRT_TARBALL_NAME}" "${LLVM_COMPILERRT_SRC_FOLDER_NAME}"
|
||||
dumpLogFile "Failed to extract the LLVM tarball"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -d "bin" ] ; then
|
||||
echo " > Deleting existing bin folder..."
|
||||
rm -rf "bin" > "${LOG_FILE}" 2>&1
|
||||
if [ $? -ne 0 ] ; then
|
||||
dumpLogFile "Failed to delete the existing bin folder"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir "bin" > "${LOG_FILE}" 2>&1
|
||||
if [ $? -ne 0 ] ; then
|
||||
dumpLogFile "Failed to create the bin folder"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " > Building libFuzzer..."
|
||||
( cd "bin" && "../${LLVM_COMPILERRT_SRC_FOLDER_NAME}/lib/Fuzzer/build.sh" ) > "${LOG_FILE}" 2>&1
|
||||
if [ $? -ne 0 ] ; then
|
||||
dumpLogFile "Failed to build the library"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "\nFinished building libFuzzer\n"
|
||||
rm "${LOG_FILE}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
checkDependencies() {
|
||||
executable_list=( "clang++" "curl" "tar" )
|
||||
|
||||
for executable in "${executable_list[@]}" ; do
|
||||
which "${executable}" > /dev/null 2>&1
|
||||
if [ $? -ne 0 ] ; then
|
||||
echo "The following program was not found: ${executable}"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
dumpLogFile() {
|
||||
if [ $# -eq 1 ] ; then
|
||||
local message="$1"
|
||||
else
|
||||
local message="An error has occurred"
|
||||
fi
|
||||
|
||||
printf "${message}\n"
|
||||
printf "Log file follows\n===\n"
|
||||
cat "${LOG_FILE}"
|
||||
printf "\n===\n"
|
||||
rm "${LOG_FILE}"
|
||||
}
|
||||
|
||||
main $@
|
||||
exit $?
|
||||
@@ -20,6 +20,7 @@
|
||||
#import "SNTCommandController.h"
|
||||
#import "SNTRule.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size) {
|
||||
if (size > 16) {
|
||||
@@ -28,7 +29,7 @@ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size
|
||||
return 1;
|
||||
}
|
||||
|
||||
santa_vnode_id_t vnodeID = {};
|
||||
SantaVnode vnodeID = {};
|
||||
std::memcpy(&vnodeID, data, size);
|
||||
|
||||
MOLXPCConnection *daemonConn = [SNTXPCControlInterface configuredConnection];
|
||||
@@ -41,14 +42,14 @@ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size
|
||||
|
||||
[[daemonConn remoteObjectProxy]
|
||||
checkCacheForVnodeID:vnodeID
|
||||
withReply:^(santa_action_t action) {
|
||||
if (action == ACTION_RESPOND_ALLOW) {
|
||||
withReply:^(SNTAction action) {
|
||||
if (action == SNTActionRespondAllow) {
|
||||
std::cerr << "File exists in [whitelist] kernel cache" << std::endl;
|
||||
;
|
||||
} else if (action == ACTION_RESPOND_DENY) {
|
||||
} else if (action == SNTActionRespondDeny) {
|
||||
std::cerr << "File exists in [blacklist] kernel cache" << std::endl;
|
||||
;
|
||||
} else if (action == ACTION_UNSET) {
|
||||
} else if (action == SNTActionUnset) {
|
||||
std::cerr << "File does not exist in cache" << std::endl;
|
||||
;
|
||||
}
|
||||
|
||||
10
LICENSE
10
LICENSE
@@ -200,3 +200,13 @@
|
||||
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.
|
||||
|
||||
------------------
|
||||
|
||||
Files: Testing/integration/VM/*
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
@@ -7,6 +7,6 @@ whichever comes first.
|
||||
|
||||
To report vulnerabilities to us privately, please e-mail `santa-team@google.com`.
|
||||
If you want to encrypt your e-mail, you can use our GPG key `0x92AFE41DAB49BBB6`
|
||||
available on pool.sks-keyservers.net:
|
||||
available on keyserver.ubuntu.com:
|
||||
|
||||
`gpg --keyserver pool.sks-keyservers.net --recv-key 0x92AFE41DAB49BBB6`
|
||||
`gpg --keyserver keyserver.ubuntu.com --recv-key 0x92AFE41DAB49BBB6`
|
||||
|
||||
@@ -31,19 +31,52 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SystemResources",
|
||||
srcs = ["SystemResources.mm"],
|
||||
hdrs = ["SystemResources.h"],
|
||||
deps = [
|
||||
":SNTLogging",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "SantaCache",
|
||||
hdrs = ["SantaCache.h"],
|
||||
deps = ["//Source/common:SNTCommon"],
|
||||
deps = [":BranchPrediction"],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SantaCacheTest",
|
||||
srcs = [
|
||||
"SantaCache.h",
|
||||
"SantaCacheTest.mm",
|
||||
srcs = ["SantaCacheTest.mm"],
|
||||
deps = [
|
||||
":SantaCache",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "BranchPrediction",
|
||||
hdrs = ["BranchPrediction.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SantaVnode",
|
||||
hdrs = ["SantaVnode.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "Platform",
|
||||
hdrs = ["Platform.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SantaVnodeHash",
|
||||
srcs = ["SantaVnodeHash.mm"],
|
||||
hdrs = ["SantaVnodeHash.h"],
|
||||
deps = [
|
||||
":SantaCache",
|
||||
":SantaVnode",
|
||||
],
|
||||
deps = ["//Source/common:SNTCommon"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
@@ -74,11 +107,11 @@ objc_library(
|
||||
|
||||
objc_library(
|
||||
name = "SNTCachedDecision",
|
||||
srcs = ["SNTCachedDecision.m"],
|
||||
srcs = ["SNTCachedDecision.mm"],
|
||||
hdrs = ["SNTCachedDecision.h"],
|
||||
deps = [
|
||||
":SNTCommon",
|
||||
":SNTCommonEnums",
|
||||
":SantaVnode",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -142,15 +175,6 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "SNTCommon",
|
||||
hdrs = ["SNTCommon.h"],
|
||||
defines = [
|
||||
"TARGET_OS_OSX",
|
||||
"TARGET_OS_MAC",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTLogging",
|
||||
srcs = ["SNTLogging.m"],
|
||||
@@ -158,12 +182,18 @@ objc_library(
|
||||
deps = [":SNTConfigurator"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "SNTPrefixTree",
|
||||
srcs = ["SNTPrefixTree.cc"],
|
||||
hdrs = ["SNTPrefixTree.h"],
|
||||
copts = ["-std=c++11"],
|
||||
deps = [":SNTLogging"],
|
||||
objc_library(
|
||||
name = "PrefixTree",
|
||||
hdrs = ["PrefixTree.h"],
|
||||
deps = [
|
||||
":SNTLogging",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "Unit",
|
||||
hdrs = ["Unit.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
@@ -281,11 +311,11 @@ objc_library(
|
||||
srcs = ["SNTXPCUnprivilegedControlInterface.m"],
|
||||
hdrs = ["SNTXPCUnprivilegedControlInterface.h"],
|
||||
deps = [
|
||||
":SNTCommon",
|
||||
":SNTCommonEnums",
|
||||
":SNTRule",
|
||||
":SNTStoredEvent",
|
||||
":SNTXPCBundleServiceInterface",
|
||||
":SantaVnode",
|
||||
"@MOLCertificate",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
@@ -307,9 +337,9 @@ santa_unit_test(
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTPrefixTreeTest",
|
||||
srcs = ["SNTPrefixTreeTest.mm"],
|
||||
deps = [":SNTPrefixTree"],
|
||||
name = "PrefixTreeTest",
|
||||
srcs = ["PrefixTreeTest.mm"],
|
||||
deps = [":PrefixTree"],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
@@ -331,11 +361,11 @@ santa_unit_test(
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":PrefixTreeTest",
|
||||
":SNTCachedDecisionTest",
|
||||
":SNTFileInfoTest",
|
||||
":SNTKVOManagerTest",
|
||||
":SNTMetricSetTest",
|
||||
":SNTPrefixTreeTest",
|
||||
":SNTRuleTest",
|
||||
":SantaCacheTest",
|
||||
],
|
||||
@@ -351,6 +381,7 @@ objc_library(
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":SystemResources",
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
],
|
||||
|
||||
22
Source/common/BranchPrediction.h
Normal file
22
Source/common/BranchPrediction.h
Normal file
@@ -0,0 +1,22 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON__BRANCHPREDICTION_H
|
||||
#define SANTA__COMMON__BRANCHPREDICTION_H
|
||||
|
||||
// Helpful macros to use when the the outcome is largely known
|
||||
#define likely(x) __builtin_expect(!!(x), 1)
|
||||
#define unlikely(x) __builtin_expect(!!(x), 0)
|
||||
|
||||
#endif
|
||||
34
Source/common/Platform.h
Normal file
34
Source/common/Platform.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON__PLATFORM_H
|
||||
#define SANTA__COMMON__PLATFORM_H
|
||||
|
||||
#include <Availability.h>
|
||||
|
||||
#if defined(MAC_OS_VERSION_12_0) && \
|
||||
MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0
|
||||
#define HAVE_MACOS_12 1
|
||||
#else
|
||||
#define HAVE_MACOS_12 0
|
||||
#endif
|
||||
|
||||
#if defined(MAC_OS_VERSION_13_0) && \
|
||||
MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_0
|
||||
#define HAVE_MACOS_13 1
|
||||
#else
|
||||
#define HAVE_MACOS_13 0
|
||||
#endif
|
||||
|
||||
#endif
|
||||
302
Source/common/PrefixTree.h
Normal file
302
Source/common/PrefixTree.h
Normal file
@@ -0,0 +1,302 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON__PREFIXTREE_H
|
||||
#define SANTA__COMMON__PREFIXTREE_H
|
||||
|
||||
#include <sys/syslimits.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
#if SANTA_PREFIX_TREE_DEBUG
|
||||
#define DEBUG_LOG LOGD
|
||||
#else
|
||||
#define DEBUG_LOG(format, ...) // NOP
|
||||
#endif
|
||||
|
||||
namespace santa::common {
|
||||
|
||||
template <typename ValueT>
|
||||
class PrefixTree {
|
||||
private:
|
||||
// Forward declaration
|
||||
enum class NodeType;
|
||||
class TreeNode;
|
||||
|
||||
public:
|
||||
PrefixTree(uint32_t max_depth = PATH_MAX)
|
||||
: root_(new TreeNode()), max_depth_(max_depth), node_count_(0) {}
|
||||
|
||||
~PrefixTree() { PruneLocked(root_); }
|
||||
|
||||
bool InsertPrefix(const char *s, ValueT value) {
|
||||
absl::MutexLock lock(&lock_);
|
||||
return InsertLocked(s, value, NodeType::kPrefix);
|
||||
}
|
||||
|
||||
bool InsertLiteral(const char *s, ValueT value) {
|
||||
absl::MutexLock lock(&lock_);
|
||||
return InsertLocked(s, value, NodeType::kLiteral);
|
||||
}
|
||||
|
||||
bool HasPrefix(const char *input) {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
return HasPrefixLocked(input);
|
||||
}
|
||||
|
||||
std::optional<ValueT> LookupLongestMatchingPrefix(const char *input) {
|
||||
if (!input) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
return LookupLongestMatchingPrefixLocked(input);
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
absl::MutexLock lock(&lock_);
|
||||
PruneLocked(root_);
|
||||
root_ = new TreeNode();
|
||||
node_count_ = 0;
|
||||
}
|
||||
|
||||
#if SANTA_PREFIX_TREE_DEBUG
|
||||
void Print() {
|
||||
char buf[max_depth_ + 1];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
PrintLocked(root_, buf, 0);
|
||||
}
|
||||
|
||||
uint32_t NodeCount() {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
return node_count_;
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_)
|
||||
bool InsertLocked(const char *input, ValueT value, NodeType node_type) {
|
||||
const char *p = input;
|
||||
TreeNode *node = root_;
|
||||
|
||||
while (*p) {
|
||||
uint8_t cur_byte = (uint8_t)*p;
|
||||
|
||||
TreeNode *child_node = node->children_[cur_byte];
|
||||
if (!child_node) {
|
||||
// Current node doesn't exist...
|
||||
// Create the rest of the nodes in the tree for the given string
|
||||
|
||||
// Keep a pointer to where this new branch starts from. If the
|
||||
// input length exceeds max_depth, the new branch will need to
|
||||
// be pruned.
|
||||
TreeNode *branch_start_node = node;
|
||||
uint8_t branch_start_byte = (uint8_t)*p;
|
||||
|
||||
do {
|
||||
TreeNode *new_node = new TreeNode();
|
||||
node->children_[cur_byte] = new_node;
|
||||
node = new_node;
|
||||
node_count_++;
|
||||
|
||||
// Check current depth...
|
||||
if (p - input >= max_depth_) {
|
||||
// Attempted to add a string that exceeded max depth
|
||||
// Prune tree from start of this new branch
|
||||
PruneLocked(branch_start_node->children_[branch_start_byte]);
|
||||
branch_start_node->children_[branch_start_byte] = nullptr;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
cur_byte = (uint8_t) * ++p;
|
||||
} while (*p);
|
||||
|
||||
node->node_type_ = node_type;
|
||||
node->value_ = value;
|
||||
|
||||
return true;
|
||||
} else if (*(p + 1) == '\0') {
|
||||
// Current node exists and we're at the end of our input...
|
||||
// Note: The current node's data will be overwritten
|
||||
|
||||
// Only increment node count if the previous node type wasn't already a
|
||||
// prefix or literal type (in which case it was already counted)
|
||||
if (child_node->node_type_ == NodeType::kInner) {
|
||||
node_count_++;
|
||||
}
|
||||
|
||||
child_node->node_type_ = node_type;
|
||||
child_node->value_ = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
node = child_node;
|
||||
p++;
|
||||
}
|
||||
|
||||
// Should only get here when input is an empty string
|
||||
return false;
|
||||
}
|
||||
|
||||
ABSL_SHARED_LOCKS_REQUIRED(lock_)
|
||||
bool HasPrefixLocked(const char *input) {
|
||||
TreeNode *node = root_;
|
||||
const char *p = input;
|
||||
|
||||
while (*p) {
|
||||
node = node->children_[(uint8_t)*p++];
|
||||
|
||||
if (!node) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (node->node_type_ == NodeType::kPrefix ||
|
||||
(*p == '\0' && node->node_type_ == NodeType::kLiteral)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ABSL_SHARED_LOCKS_REQUIRED(lock_)
|
||||
std::optional<ValueT> LookupLongestMatchingPrefixLocked(const char *input) {
|
||||
TreeNode *node = root_;
|
||||
TreeNode *match = nullptr;
|
||||
const char *p = input;
|
||||
|
||||
while (*p) {
|
||||
node = node->children_[(uint8_t)*p++];
|
||||
|
||||
if (!node) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (node->node_type_ == NodeType::kPrefix ||
|
||||
(*p == '\0' && node->node_type_ == NodeType::kLiteral)) {
|
||||
match = node;
|
||||
}
|
||||
}
|
||||
|
||||
return match ? std::make_optional<ValueT>(match->value_) : std::nullopt;
|
||||
}
|
||||
|
||||
ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_)
|
||||
void PruneLocked(TreeNode *target) {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For deep trees, a recursive approach will generate too many stack frames.
|
||||
// Since the depth of the tree is configurable, err on the side of caution
|
||||
// and use a "stack" to walk the tree in a non-recursive manner.
|
||||
TreeNode **stack = new TreeNode *[node_count_ + 1];
|
||||
if (!stack) {
|
||||
LOGE(@"Unable to prune tree!");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t count = 0;
|
||||
|
||||
// Seed the "stack" with a starting node.
|
||||
stack[count++] = target;
|
||||
|
||||
// Start at the target node and walk the tree to find and delete all the
|
||||
// sub-nodes.
|
||||
while (count) {
|
||||
TreeNode *node = stack[--count];
|
||||
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
if (!node->children_[i]) {
|
||||
continue;
|
||||
}
|
||||
stack[count++] = node->children_[i];
|
||||
}
|
||||
|
||||
delete node;
|
||||
--node_count_;
|
||||
}
|
||||
|
||||
delete[] stack;
|
||||
}
|
||||
|
||||
#if SANTA_PREFIX_TREE_DEBUG
|
||||
ABSL_SHARED_LOCKS_REQUIRED(lock_)
|
||||
void PrintLocked(TreeNode *node, char *buf, uint32_t depth) {
|
||||
for (size_t i = 0; i < 256; i++) {
|
||||
TreeNode *cur_node = node->children_[i];
|
||||
if (cur_node) {
|
||||
buf[depth] = i;
|
||||
if (cur_node->node_type_ != NodeType::kInner) {
|
||||
printf("\t%s (type: %s)\n", buf,
|
||||
cur_node->node_type_ == NodeType::kPrefix ? "prefix" : "literal");
|
||||
}
|
||||
PrintLocked(cur_node, buf, depth + 1);
|
||||
buf[depth] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
enum class NodeType {
|
||||
kInner = 0,
|
||||
kPrefix,
|
||||
kLiteral,
|
||||
};
|
||||
|
||||
///
|
||||
/// TreeNode is a wrapper class that represents one byte.
|
||||
/// 1 node can represent a whole ASCII character.
|
||||
/// For example a pointer to the 'A' node will be stored at children[0x41].
|
||||
/// It takes 1-4 nodes to represent a UTF-8 encoded Unicode character.
|
||||
///
|
||||
/// The path for "/🤘" would look like this:
|
||||
/// children[0x2f] -> children[0xf0] -> children[0x9f] -> children[0xa4]
|
||||
/// -> children[0x98]
|
||||
///
|
||||
/// The path for "/dev" is:
|
||||
/// children[0x2f] -> children[0x64] -> children[0x65] -> children[0x76]
|
||||
///
|
||||
/// Lookups of children are O(1).
|
||||
///
|
||||
/// Having the nodes represented by a smaller width, such as a nibble (1/2
|
||||
/// byte), would drastically decrease the memory footprint but would double
|
||||
/// required dereferences.
|
||||
///
|
||||
/// TODO(bur): Potentially convert this into a full on radix tree.
|
||||
///
|
||||
class TreeNode {
|
||||
public:
|
||||
TreeNode() : children_(), node_type_(NodeType::kInner) {}
|
||||
~TreeNode() = default;
|
||||
TreeNode *children_[256];
|
||||
PrefixTree::NodeType node_type_;
|
||||
ValueT value_;
|
||||
};
|
||||
|
||||
TreeNode *root_;
|
||||
const uint32_t max_depth_;
|
||||
uint32_t node_count_ ABSL_GUARDED_BY(lock_);
|
||||
absl::Mutex lock_;
|
||||
};
|
||||
|
||||
} // namespace santa::common
|
||||
|
||||
#endif
|
||||
224
Source/common/PrefixTreeTest.mm
Normal file
224
Source/common/PrefixTreeTest.mm
Normal file
@@ -0,0 +1,224 @@
|
||||
/// Copyright 2022 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 <XCTest/XCTest.h>
|
||||
|
||||
#define SANTA_PREFIX_TREE_DEBUG 1
|
||||
#include "Source/common/PrefixTree.h"
|
||||
|
||||
using santa::common::PrefixTree;
|
||||
|
||||
@interface PrefixTreeTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation PrefixTreeTest
|
||||
|
||||
- (void)testBasic {
|
||||
PrefixTree<int> tree;
|
||||
|
||||
XCTAssertFalse(tree.HasPrefix("/foo/bar/baz"));
|
||||
XCTAssertFalse(tree.HasPrefix("/foo/bar.txt"));
|
||||
XCTAssertFalse(tree.HasPrefix("/baz"));
|
||||
|
||||
XCTAssertTrue(tree.InsertPrefix("/foo", 12));
|
||||
XCTAssertTrue(tree.InsertPrefix("/bar", 34));
|
||||
XCTAssertTrue(tree.InsertLiteral("/foo/bar", 56));
|
||||
|
||||
// Re-inserting something that exists is allowed
|
||||
XCTAssertTrue(tree.InsertLiteral("/foo", 78));
|
||||
XCTAssertTrue(tree.InsertPrefix("/foo", 56));
|
||||
|
||||
XCTAssertTrue(tree.HasPrefix("/foo/bar/baz"));
|
||||
XCTAssertTrue(tree.HasPrefix("/foo/bar.txt"));
|
||||
XCTAssertFalse(tree.HasPrefix("/baz"));
|
||||
|
||||
// Empty strings are not supported
|
||||
XCTAssertFalse(tree.InsertLiteral("", 0));
|
||||
XCTAssertFalse(tree.InsertPrefix("", 0));
|
||||
}
|
||||
|
||||
- (void)testHasPrefix {
|
||||
PrefixTree<int> tree;
|
||||
|
||||
XCTAssertTrue(tree.InsertPrefix("/foo", 0));
|
||||
XCTAssertTrue(tree.InsertLiteral("/bar", 0));
|
||||
XCTAssertTrue(tree.InsertLiteral("/baz", 0));
|
||||
XCTAssertTrue(tree.InsertLiteral("/qaz", 0));
|
||||
|
||||
// Check that a tree with a matching prefix is successful
|
||||
XCTAssertTrue(tree.HasPrefix("/foo.txt"));
|
||||
|
||||
// This shouldn't succeed because `/bar` `/baz` and `qaz` are literals
|
||||
XCTAssertFalse(tree.HasPrefix("/bar.txt"));
|
||||
XCTAssertFalse(tree.HasPrefix("/baz.txt"));
|
||||
XCTAssertFalse(tree.HasPrefix("/qaz.txt"));
|
||||
|
||||
// Now change `/bar` to a prefix type and retest HasPrefix
|
||||
// `/bar.txt` should now succeed, but `/baz.txt` should still not pass
|
||||
XCTAssertTrue(tree.InsertPrefix("/bar", 0));
|
||||
XCTAssertTrue(tree.HasPrefix("/bar.txt"));
|
||||
XCTAssertFalse(tree.HasPrefix("/baz.txt"));
|
||||
XCTAssertFalse(tree.HasPrefix("/qaz.txt"));
|
||||
|
||||
// Insert a new prefix string to allow `/baz.txt` to have a valid prefix
|
||||
XCTAssertTrue(tree.InsertPrefix("/b", 0));
|
||||
XCTAssertTrue(tree.HasPrefix("/baz.txt"));
|
||||
XCTAssertFalse(tree.HasPrefix("/qaz.txt"));
|
||||
|
||||
// An exact match on a literal allows HasPrefix to succeed
|
||||
XCTAssertTrue(tree.InsertLiteral("/qaz.txt", 0));
|
||||
XCTAssertTrue(tree.HasPrefix("/qaz.txt"));
|
||||
}
|
||||
|
||||
- (void)testLookupLongestMatchingPrefix {
|
||||
PrefixTree<int> tree;
|
||||
|
||||
XCTAssertTrue(tree.InsertPrefix("/foo", 12));
|
||||
XCTAssertTrue(tree.InsertPrefix("/bar", 34));
|
||||
XCTAssertTrue(tree.InsertPrefix("/foo/bar.txt", 56));
|
||||
|
||||
std::optional<int> value;
|
||||
|
||||
// Matching exact prefix
|
||||
value = tree.LookupLongestMatchingPrefix("/foo");
|
||||
XCTAssertEqual(value.value_or(0), 12);
|
||||
|
||||
// Ensure changing node type works as expected
|
||||
// Literals must match exactly.
|
||||
value = tree.LookupLongestMatchingPrefix("/foo/bar.txt.tmp");
|
||||
XCTAssertEqual(value.value_or(0), 56);
|
||||
XCTAssertTrue(tree.InsertLiteral("/foo/bar.txt", 90));
|
||||
value = tree.LookupLongestMatchingPrefix("/foo/bar.txt.tmp");
|
||||
XCTAssertEqual(value.value_or(0), 12);
|
||||
|
||||
// Inserting over an exiting node returns the new value
|
||||
XCTAssertTrue(tree.InsertPrefix("/foo", 78));
|
||||
value = tree.LookupLongestMatchingPrefix("/foo");
|
||||
XCTAssertEqual(value.value_or(0), 78);
|
||||
|
||||
// No matching prefix
|
||||
value = tree.LookupLongestMatchingPrefix("/asdf");
|
||||
XCTAssertEqual(value.value_or(0), 0);
|
||||
}
|
||||
|
||||
- (void)testNodeCounts {
|
||||
const uint32_t maxDepth = 100;
|
||||
PrefixTree<int> tree(100);
|
||||
|
||||
XCTAssertEqual(tree.NodeCount(), 0);
|
||||
|
||||
// Start with a small string
|
||||
XCTAssertTrue(tree.InsertPrefix("asdf", 0));
|
||||
XCTAssertEqual(tree.NodeCount(), 4);
|
||||
|
||||
// Add a couple more characters to the existing string
|
||||
XCTAssertTrue(tree.InsertPrefix("asdfgh", 0));
|
||||
XCTAssertEqual(tree.NodeCount(), 6);
|
||||
|
||||
// Inserting a string that exceeds max depth doesn't increase node count
|
||||
XCTAssertFalse(tree.InsertPrefix(std::string(maxDepth + 10, 'A').c_str(), 0));
|
||||
XCTAssertEqual(tree.NodeCount(), 6);
|
||||
|
||||
// Add a new string that is a prefix of an existing string
|
||||
// This should increment the count by one since a new terminal node exists
|
||||
XCTAssertTrue(tree.InsertPrefix("as", 0));
|
||||
XCTAssertEqual(tree.NodeCount(), 7);
|
||||
|
||||
// Re-inserting onto an existing node shouldn't modify the count
|
||||
tree.InsertLiteral("as", 0);
|
||||
tree.InsertPrefix("as", 0);
|
||||
XCTAssertEqual(tree.NodeCount(), 7);
|
||||
}
|
||||
|
||||
- (void)testReset {
|
||||
// Ensure resetting a tree removes all content
|
||||
PrefixTree<int> tree;
|
||||
|
||||
tree.Reset();
|
||||
XCTAssertEqual(tree.NodeCount(), 0);
|
||||
|
||||
XCTAssertTrue(tree.InsertPrefix("asdf", 0));
|
||||
XCTAssertTrue(tree.InsertPrefix("qwerty", 0));
|
||||
|
||||
XCTAssertTrue(tree.HasPrefix("asdf"));
|
||||
XCTAssertTrue(tree.HasPrefix("qwerty"));
|
||||
XCTAssertEqual(tree.NodeCount(), 10);
|
||||
|
||||
tree.Reset();
|
||||
XCTAssertFalse(tree.HasPrefix("asdf"));
|
||||
XCTAssertFalse(tree.HasPrefix("qwerty"));
|
||||
XCTAssertEqual(tree.NodeCount(), 0);
|
||||
}
|
||||
|
||||
- (void)testComplexValues {
|
||||
class Foo {
|
||||
public:
|
||||
Foo(int x) : x_(x) {}
|
||||
int X() { return x_; }
|
||||
|
||||
private:
|
||||
int x_;
|
||||
};
|
||||
|
||||
PrefixTree<std::shared_ptr<Foo>> tree;
|
||||
|
||||
XCTAssertTrue(tree.InsertPrefix("foo", std::make_shared<Foo>(123)));
|
||||
XCTAssertTrue(tree.InsertPrefix("bar", std::make_shared<Foo>(456)));
|
||||
|
||||
std::optional<std::shared_ptr<Foo>> value;
|
||||
value = tree.LookupLongestMatchingPrefix("foo");
|
||||
XCTAssertTrue(value.has_value() && value->get()->X() == 123);
|
||||
|
||||
value = tree.LookupLongestMatchingPrefix("bar");
|
||||
XCTAssertTrue(value.has_value() && value->get()->X() == 456);
|
||||
|
||||
value = tree.LookupLongestMatchingPrefix("asdf");
|
||||
XCTAssertFalse(value.has_value());
|
||||
}
|
||||
|
||||
- (void)testThreading {
|
||||
uint32_t count = 4096;
|
||||
auto t = new PrefixTree<int>(count * (uint32_t)[NSUUID UUID].UUIDString.length);
|
||||
|
||||
NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
|
||||
for (int i = 0; i < count; ++i) {
|
||||
[UUIDs addObject:[NSUUID UUID].UUIDString];
|
||||
}
|
||||
|
||||
__block BOOL stop = NO;
|
||||
|
||||
// Create a bunch of background noise.
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
for (uint64_t i = 0; i < UINT64_MAX; ++i) {
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
t->HasPrefix([UUIDs[i % count] UTF8String]);
|
||||
});
|
||||
if (stop) return;
|
||||
}
|
||||
});
|
||||
|
||||
// Fill up the tree.
|
||||
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
|
||||
XCTAssertEqual(t->InsertPrefix([UUIDs[i] UTF8String], 0), true);
|
||||
});
|
||||
|
||||
// Make sure every leaf byte is found.
|
||||
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
|
||||
XCTAssertTrue(t->HasPrefix([UUIDs[i] UTF8String]));
|
||||
});
|
||||
|
||||
stop = YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -15,8 +15,8 @@
|
||||
#import <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
|
||||
@class MOLCertificate;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile;
|
||||
|
||||
@property santa_vnode_id_t vnodeId;
|
||||
@property SantaVnode vnodeId;
|
||||
@property SNTEventState decision;
|
||||
@property NSString *decisionExtra;
|
||||
@property NSString *sha256;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -19,8 +20,7 @@
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_vnodeId.fsid = (uint64_t)esFile->stat.st_dev;
|
||||
_vnodeId.fileid = esFile->stat.st_ino;
|
||||
_vnodeId = SantaVnode::VnodeForFile(esFile);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/// Copyright 2015-2022 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.
|
||||
|
||||
///
|
||||
/// Common defines between daemon <-> client
|
||||
///
|
||||
|
||||
#ifndef SANTA__COMMON__COMMON_H
|
||||
#define SANTA__COMMON__COMMON_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sys/param.h>
|
||||
|
||||
// Branch prediction
|
||||
#define likely(x) __builtin_expect(!!(x), 1)
|
||||
#define unlikely(x) __builtin_expect(!!(x), 0)
|
||||
|
||||
typedef enum {
|
||||
ACTION_UNSET,
|
||||
|
||||
// REQUESTS
|
||||
// If an operation is awaiting a cache decision from a similar operation
|
||||
// currently being processed, it will poll about every 5 ms for an answer.
|
||||
ACTION_REQUEST_BINARY,
|
||||
|
||||
// RESPONSES
|
||||
ACTION_RESPOND_ALLOW,
|
||||
ACTION_RESPOND_DENY,
|
||||
ACTION_RESPOND_ALLOW_COMPILER,
|
||||
} santa_action_t;
|
||||
|
||||
#define RESPONSE_VALID(x) \
|
||||
(x == ACTION_RESPOND_ALLOW || x == ACTION_RESPOND_DENY || \
|
||||
x == ACTION_RESPOND_ALLOW_COMPILER)
|
||||
|
||||
// Struct to manage vnode IDs
|
||||
typedef struct santa_vnode_id_t {
|
||||
uint64_t fsid;
|
||||
uint64_t fileid;
|
||||
|
||||
#ifdef __cplusplus
|
||||
bool operator==(const santa_vnode_id_t &rhs) const {
|
||||
return fsid == rhs.fsid && fileid == rhs.fileid;
|
||||
}
|
||||
#endif
|
||||
} santa_vnode_id_t;
|
||||
|
||||
#endif // SANTA__COMMON__COMMON_H
|
||||
@@ -19,6 +19,23 @@
|
||||
/// The integer values are also stored in the database and so shouldn't be changed.
|
||||
///
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTAction) {
|
||||
SNTActionUnset,
|
||||
|
||||
// REQUESTS
|
||||
// If an operation is awaiting a cache decision from a similar operation
|
||||
// currently being processed, it will poll about every 5 ms for an answer.
|
||||
SNTActionRequestBinary,
|
||||
|
||||
// RESPONSES
|
||||
SNTActionRespondAllow,
|
||||
SNTActionRespondDeny,
|
||||
SNTActionRespondAllowCompiler,
|
||||
};
|
||||
|
||||
#define RESPONSE_VALID(x) \
|
||||
(x == SNTActionRespondAllow || x == SNTActionRespondDeny || x == SNTActionRespondAllowCompiler)
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTRuleType) {
|
||||
SNTRuleTypeUnknown,
|
||||
|
||||
@@ -118,6 +135,17 @@ typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
|
||||
SNTMetricFormatTypeMonarchJSON,
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
enum class FileAccessPolicyDecision {
|
||||
kNoPolicy,
|
||||
kDenied,
|
||||
kDeniedInvalidSignature,
|
||||
kAllowed,
|
||||
kAllowedReadAccess,
|
||||
kAllowedAuditOnly,
|
||||
};
|
||||
#endif
|
||||
|
||||
static const char *kSantaDPath =
|
||||
"/Applications/Santa.app/Contents/Library/SystemExtensions/"
|
||||
"com.google.santa.daemon.systemextension/Contents/MacOS/com.google.santa.daemon";
|
||||
|
||||
@@ -238,6 +238,23 @@
|
||||
///
|
||||
@property(readonly, nonatomic) float spoolDirectoryEventMaxFlushTimeSec;
|
||||
|
||||
///
|
||||
/// If set, contains the path to the filesystem access policy config plist.
|
||||
///
|
||||
/// @note: This property is KVO compliant, but is only read once at santad startup.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *fileAccessPolicyPlist;
|
||||
|
||||
///
|
||||
/// If fileAccessPolicyPlist is set, fileAccessPolicyUpdateIntervalSec
|
||||
/// sets the number of seconds between times that the configuration file is
|
||||
/// re-read and policies reconstructed.
|
||||
/// Defaults to 600 seconds (10 minutes)
|
||||
///
|
||||
/// @note: This property is KVO compliant, but should only be read once at santad startup.
|
||||
///
|
||||
@property(readonly, nonatomic) uint32_t fileAccessPolicyUpdateIntervalSec;
|
||||
|
||||
///
|
||||
/// Enabling this appends the Santa machine ID to the end of each log line. If nothing
|
||||
/// has been overriden, this is the host's UUID.
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
/// The hard-coded path to the sync state file.
|
||||
NSString *const kSyncStateFilePath = @"/var/db/santa/sync-state.plist";
|
||||
|
||||
#ifdef DEBUG
|
||||
NSString *const kConfigOverrideFilePath = @"/var/db/santa/config-overrides.plist";
|
||||
#endif
|
||||
|
||||
/// The domain used by mobileconfig.
|
||||
static NSString *const kMobileConfigDomain = @"com.google.santa";
|
||||
|
||||
@@ -93,6 +97,9 @@ static NSString *const kSpoolDirectoryFileSizeThresholdKB = @"SpoolDirectoryFile
|
||||
static NSString *const kSpoolDirectorySizeThresholdMB = @"SpoolDirectorySizeThresholdMB";
|
||||
static NSString *const kSpoolDirectoryEventMaxFlushTimeSec = @"SpoolDirectoryEventMaxFlushTimeSec";
|
||||
|
||||
static NSString *const kFileAccessPolicyPlist = @"FileAccessPolicyPlist";
|
||||
static NSString *const kFileAccessPolicyUpdateIntervalSec = @"FileAccessPolicyUpdateIntervalSec";
|
||||
|
||||
static NSString *const kEnableMachineIDDecoration = @"EnableMachineIDDecoration";
|
||||
|
||||
static NSString *const kEnableForkAndExitLogging = @"EnableForkAndExitLogging";
|
||||
@@ -204,6 +211,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kSpoolDirectoryFileSizeThresholdKB : number,
|
||||
kSpoolDirectorySizeThresholdMB : number,
|
||||
kSpoolDirectoryEventMaxFlushTimeSec : number,
|
||||
kFileAccessPolicyPlist : string,
|
||||
kFileAccessPolicyUpdateIntervalSec : number,
|
||||
kEnableMachineIDDecoration : number,
|
||||
kEnableForkAndExitLogging : number,
|
||||
kIgnoreOtherEndpointSecurityClients : number,
|
||||
@@ -407,6 +416,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyPlist {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyUpdateIntervalSec {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableMachineIDDecoration {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -785,6 +802,16 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
: 15.0;
|
||||
}
|
||||
|
||||
- (NSString *)fileAccessPolicyPlist {
|
||||
return self.configState[kFileAccessPolicyPlist];
|
||||
}
|
||||
|
||||
- (uint32_t)fileAccessPolicyUpdateIntervalSec {
|
||||
return self.configState[kFileAccessPolicyUpdateIntervalSec]
|
||||
? [self.configState[kFileAccessPolicyUpdateIntervalSec] unsignedIntValue]
|
||||
: 60 * 10;
|
||||
}
|
||||
|
||||
- (BOOL)enableMachineIDDecoration {
|
||||
NSNumber *number = self.configState[kEnableMachineIDDecoration];
|
||||
return number ? [number boolValue] : NO;
|
||||
@@ -992,6 +1019,18 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
forcedConfig[key] = [self expressionForPattern:pattern];
|
||||
}
|
||||
}
|
||||
#ifdef DEBUG
|
||||
NSDictionary *overrides = [NSDictionary dictionaryWithContentsOfFile:kConfigOverrideFilePath];
|
||||
for (NSString *key in overrides) {
|
||||
id obj = overrides[key];
|
||||
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]]) continue;
|
||||
forcedConfig[key] = obj;
|
||||
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
|
||||
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;
|
||||
forcedConfig[key] = [self expressionForPattern:pattern];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return forcedConfig;
|
||||
}
|
||||
|
||||
@@ -1008,12 +1047,50 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
selector:@selector(defaultsChanged:)
|
||||
name:NSUserDefaultsDidChangeNotification
|
||||
object:nil];
|
||||
#ifdef DEBUG
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
[self watchOverridesFile];
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
- (void)watchOverridesFile {
|
||||
while (![[NSFileManager defaultManager] fileExistsAtPath:kConfigOverrideFilePath]) {
|
||||
[NSThread sleepForTimeInterval:0.2];
|
||||
}
|
||||
[self defaultsChanged:nil];
|
||||
|
||||
int descriptor = open([kConfigOverrideFilePath fileSystemRepresentation], O_EVTONLY);
|
||||
if (descriptor < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_source_t source =
|
||||
dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, descriptor,
|
||||
DISPATCH_VNODE_WRITE | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_DELETE,
|
||||
dispatch_get_global_queue(QOS_CLASS_UTILITY, 0));
|
||||
dispatch_source_set_event_handler(source, ^{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self defaultsChanged:nil];
|
||||
});
|
||||
unsigned long events = dispatch_source_get_data(source);
|
||||
if ((events & DISPATCH_VNODE_DELETE) || (events & DISPATCH_VNODE_RENAME)) {
|
||||
dispatch_source_cancel(source);
|
||||
}
|
||||
});
|
||||
dispatch_source_set_cancel_handler(source, ^{
|
||||
close(descriptor);
|
||||
[self watchOverridesFile];
|
||||
});
|
||||
dispatch_resume(source);
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)defaultsChanged:(void *)v {
|
||||
SEL handleChange = @selector(handleChange);
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:handleChange object:nil];
|
||||
[self performSelector:handleChange withObject:nil afterDelay:5.0f];
|
||||
[self performSelector:handleChange withObject:nil afterDelay:1.0f];
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
@@ -572,6 +572,10 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
NSData *cmdData = [self safeSubdataWithRange:NSMakeRange(offset, sz_segment)];
|
||||
if (!cmdData) return nil;
|
||||
|
||||
if (((struct load_command *)[cmdData bytes])->cmdsize < sizeof(struct load_command)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (is64) {
|
||||
struct segment_command_64 *lc = (struct segment_command_64 *)[cmdData bytes];
|
||||
if (lc->cmd == LC_SEGMENT_64 && memcmp(lc->segname, "__TEXT", 6) == 0) {
|
||||
@@ -642,7 +646,10 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
///
|
||||
- (NSData *)safeSubdataWithRange:(NSRange)range {
|
||||
@try {
|
||||
if ((range.location + range.length) > self.fileSize) return nil;
|
||||
NSUInteger size;
|
||||
if (__builtin_add_overflow(range.location, range.length, &size) || size > self.fileSize) {
|
||||
return nil;
|
||||
}
|
||||
[self.fileHandle seekToFileOffset:range.location];
|
||||
NSData *d = [self.fileHandle readDataOfLength:range.length];
|
||||
if (d.length != range.length) return nil;
|
||||
|
||||
@@ -34,7 +34,12 @@
|
||||
- (void)testPathStandardizing {
|
||||
SNTFileInfo *sut = [[SNTFileInfo alloc] initWithPath:@"/Applications/Safari.app"];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.path, @"/Applications/Safari.app/Contents/MacOS/Safari");
|
||||
if (@available(macOS 13.0, *)) {
|
||||
XCTAssertEqualObjects(sut.path, @"/System/Volumes/Preboot/Cryptexes/App/System/Applications/"
|
||||
@"Safari.app/Contents/MacOS/Safari");
|
||||
} else {
|
||||
XCTAssertEqualObjects(sut.path, @"/Applications/Safari.app/Contents/MacOS/Safari");
|
||||
}
|
||||
|
||||
sut = [[SNTFileInfo alloc] initWithPath:@"../../../../../../../../../../../../../../../bin/ls"];
|
||||
XCTAssertEqualObjects(sut.path, @"/bin/ls");
|
||||
@@ -90,6 +95,11 @@
|
||||
}
|
||||
|
||||
- (void)testKext {
|
||||
// Skip this test on macOS 13 as KEXTs have moved into the kernelcache.
|
||||
if (@available(macOS 13.0, *)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SNTFileInfo *sut = [[SNTFileInfo alloc]
|
||||
initWithPath:@"/System/Library/Extensions/AppleAPIC.kext/Contents/MacOS/AppleAPIC"];
|
||||
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
/// Copyright 2018 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 "Source/common/SNTPrefixTree.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#define LOGD(format, ...) // NOP
|
||||
#define LOGE(format, ...) // NOP
|
||||
|
||||
#define lck_rw_lock_shared(l) pthread_rwlock_rdlock(&l)
|
||||
#define lck_rw_unlock_shared(l) pthread_rwlock_unlock(&l)
|
||||
#define lck_rw_lock_exclusive(l) pthread_rwlock_wrlock(&l)
|
||||
#define lck_rw_unlock_exclusive(l) pthread_rwlock_unlock(&l)
|
||||
|
||||
#define lck_rw_lock_shared_to_exclusive(l) \
|
||||
({ \
|
||||
pthread_rwlock_unlock(&l); \
|
||||
false; \
|
||||
})
|
||||
#define lck_rw_lock_exclusive_to_shared(l) \
|
||||
({ \
|
||||
pthread_rwlock_unlock(&l); \
|
||||
pthread_rwlock_rdlock(&l); \
|
||||
})
|
||||
|
||||
#define lck_mtx_lock(l) l->lock()
|
||||
#define lck_mtx_unlock(l) l->unlock()
|
||||
|
||||
SNTPrefixTree::SNTPrefixTree(uint32_t max_nodes) {
|
||||
root_ = new SantaPrefixNode();
|
||||
node_count_ = 0;
|
||||
max_nodes_ = max_nodes;
|
||||
|
||||
pthread_rwlock_init(&spt_lock_, nullptr);
|
||||
spt_add_lock_ = new std::mutex;
|
||||
}
|
||||
|
||||
IOReturn SNTPrefixTree::AddPrefix(const char *prefix, uint64_t *node_count) {
|
||||
// Serialize requests to AddPrefix. Otherwise one AddPrefix thread could
|
||||
// overwrite whole branches of another. HasPrefix is still free to read the
|
||||
// tree, until AddPrefix needs to modify it.
|
||||
lck_mtx_lock(spt_add_lock_);
|
||||
|
||||
// Don't allow an empty prefix.
|
||||
if (prefix[0] == '\0') return kIOReturnBadArgument;
|
||||
|
||||
LOGD("Trying to add prefix: %s", prefix);
|
||||
|
||||
// Enforce max tree depth.
|
||||
size_t len = strnlen(prefix, max_nodes_);
|
||||
|
||||
// Grab a shared lock until a new branch is required.
|
||||
lck_rw_lock_shared(spt_lock_);
|
||||
|
||||
SantaPrefixNode *node = root_;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
// If there is a node in the path that is considered a prefix, stop adding.
|
||||
// For our purposes we only care about the shortest path that matches.
|
||||
if (node->isPrefix) break;
|
||||
|
||||
// Only process a byte at a time.
|
||||
uint8_t value = (uint8_t)prefix[i];
|
||||
|
||||
// Create the child if it does not exist.
|
||||
if (!node->children[value]) {
|
||||
// Upgrade the shared lock.
|
||||
// If the upgrade fails, the shared lock is released.
|
||||
if (!lck_rw_lock_shared_to_exclusive(spt_lock_)) {
|
||||
// Grab a new exclusive lock.
|
||||
lck_rw_lock_exclusive(spt_lock_);
|
||||
}
|
||||
|
||||
// Is there enough room for the rest of the prefix?
|
||||
if ((node_count_ + (len - i)) > max_nodes_) {
|
||||
LOGE("Prefix tree is full, can not add: %s", prefix);
|
||||
|
||||
if (node_count) *node_count = node_count_;
|
||||
lck_rw_unlock_exclusive(spt_lock_);
|
||||
lck_mtx_unlock(spt_add_lock_);
|
||||
return kIOReturnNoResources;
|
||||
}
|
||||
|
||||
// Create the rest of the prefix.
|
||||
while (i < len) {
|
||||
value = (uint8_t)prefix[i++];
|
||||
|
||||
SantaPrefixNode *new_node = new SantaPrefixNode();
|
||||
node->children[value] = new_node;
|
||||
++node_count_;
|
||||
|
||||
node = new_node;
|
||||
}
|
||||
|
||||
// This is the end, mark the node as a prefix.
|
||||
LOGD("Added prefix: %s", prefix);
|
||||
|
||||
node->isPrefix = true;
|
||||
|
||||
// Downgrade the exclusive lock
|
||||
lck_rw_lock_exclusive_to_shared(spt_lock_);
|
||||
} else if (i + 1 == len) {
|
||||
// If the child does exist and it is the end...
|
||||
// Set the new, higher prefix and prune the now dead nodes.
|
||||
|
||||
if (!lck_rw_lock_shared_to_exclusive(spt_lock_)) {
|
||||
lck_rw_lock_exclusive(spt_lock_);
|
||||
}
|
||||
|
||||
PruneNode(node->children[value]);
|
||||
|
||||
SantaPrefixNode *new_node = new SantaPrefixNode();
|
||||
new_node->isPrefix = true;
|
||||
|
||||
node->children[value] = new_node;
|
||||
++node_count_;
|
||||
|
||||
LOGD("Added prefix: %s", prefix);
|
||||
|
||||
lck_rw_lock_exclusive_to_shared(spt_lock_);
|
||||
}
|
||||
|
||||
// Get ready for the next iteration.
|
||||
node = node->children[value];
|
||||
}
|
||||
|
||||
if (node_count) *node_count = node_count_;
|
||||
|
||||
lck_rw_unlock_shared(spt_lock_);
|
||||
lck_mtx_unlock(spt_add_lock_);
|
||||
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
|
||||
bool SNTPrefixTree::HasPrefix(const char *string) {
|
||||
lck_rw_lock_shared(spt_lock_);
|
||||
|
||||
auto found = false;
|
||||
|
||||
SantaPrefixNode *node = root_;
|
||||
|
||||
// A well formed tree will always break this loop. Even if string doesn't
|
||||
// terminate.
|
||||
const char *p = string;
|
||||
while (*p) {
|
||||
// Only process a byte at a time.
|
||||
node = node->children[(uint8_t)*p++];
|
||||
|
||||
// If it doesn't exist in the tree, no match.
|
||||
if (!node) break;
|
||||
|
||||
// If it does exist, is it a prefix?
|
||||
if (node->isPrefix) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lck_rw_unlock_shared(spt_lock_);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
void SNTPrefixTree::Reset() {
|
||||
lck_rw_lock_exclusive(spt_lock_);
|
||||
|
||||
PruneNode(root_);
|
||||
root_ = new SantaPrefixNode();
|
||||
node_count_ = 0;
|
||||
|
||||
lck_rw_unlock_exclusive(spt_lock_);
|
||||
}
|
||||
|
||||
void SNTPrefixTree::PruneNode(SantaPrefixNode *target) {
|
||||
if (!target) return;
|
||||
|
||||
// For deep trees, a recursive approach will generate too many stack frames.
|
||||
// Make a "stack" and walk the tree.
|
||||
auto stack = new SantaPrefixNode *[node_count_ + 1];
|
||||
if (!stack) {
|
||||
LOGE("Unable to prune tree!");
|
||||
|
||||
return;
|
||||
}
|
||||
auto count = 0;
|
||||
|
||||
// Seed the "stack" with a starting node.
|
||||
stack[count++] = target;
|
||||
|
||||
// Start at the target node and walk the tree to find and delete all the
|
||||
// sub-nodes.
|
||||
while (count) {
|
||||
auto node = stack[--count];
|
||||
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
if (!node->children[i]) continue;
|
||||
stack[count++] = node->children[i];
|
||||
}
|
||||
|
||||
delete node;
|
||||
--node_count_;
|
||||
}
|
||||
|
||||
delete[] stack;
|
||||
}
|
||||
|
||||
SNTPrefixTree::~SNTPrefixTree() {
|
||||
lck_rw_lock_exclusive(spt_lock_);
|
||||
PruneNode(root_);
|
||||
root_ = nullptr;
|
||||
lck_rw_unlock_exclusive(spt_lock_);
|
||||
|
||||
pthread_rwlock_destroy(&spt_lock_);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/// Copyright 2018 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTA_DRIVER__SANTAPREFIXTREE_H
|
||||
#define SANTA__SANTA_DRIVER__SANTAPREFIXTREE_H
|
||||
|
||||
#include <IOKit/IOReturn.h>
|
||||
#include <sys/param.h>
|
||||
|
||||
// Support for unit testing.
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
///
|
||||
/// SantaPrefixTree is a simple prefix tree implementation.
|
||||
/// Operations are thread safe.
|
||||
///
|
||||
class SNTPrefixTree {
|
||||
public:
|
||||
// Add a prefix to the tree.
|
||||
// Optionally pass node_count to get the number of nodes after the add.
|
||||
IOReturn AddPrefix(const char *, uint64_t *node_count = nullptr);
|
||||
|
||||
// Check if the tree has a prefix for string.
|
||||
bool HasPrefix(const char *string);
|
||||
|
||||
// Reset the tree.
|
||||
void Reset();
|
||||
|
||||
SNTPrefixTree(uint32_t max_nodes = kDefaultMaxNodes);
|
||||
~SNTPrefixTree();
|
||||
|
||||
private:
|
||||
///
|
||||
/// SantaPrefixNode is a wrapper class that represents one byte.
|
||||
/// 1 node can represent a whole ASCII character.
|
||||
/// For example a pointer to the 'A' node will be stored at children[0x41].
|
||||
/// It takes 1-4 nodes to represent a UTF-8 encoded Unicode character.
|
||||
///
|
||||
/// The path for "/🤘" would look like this:
|
||||
/// children[0x2f] -> children[0xf0] -> children[0x9f] -> children[0xa4]
|
||||
/// -> children[0x98]
|
||||
///
|
||||
/// The path for "/dev" is:
|
||||
/// children[0x2f] -> children[0x64] -> children[0x65] -> children[0x76]
|
||||
///
|
||||
/// Lookups of children are O(1).
|
||||
///
|
||||
/// Having the nodes represented by a smaller width, such as a nibble (1/2
|
||||
/// byte), would drastically decrease the memory footprint but would double
|
||||
/// required dereferences.
|
||||
///
|
||||
/// TODO(bur): Potentially convert this into a full on radix tree.
|
||||
///
|
||||
class SantaPrefixNode {
|
||||
public:
|
||||
bool isPrefix;
|
||||
SantaPrefixNode *children[256];
|
||||
};
|
||||
|
||||
// PruneNode will remove the passed in node from the tree.
|
||||
// The passed in node and all subnodes will be deleted.
|
||||
// It is the caller's responsibility to reset the pointer to this node (held
|
||||
// by the parent). If the tree is in use grab the exclusive lock.
|
||||
void PruneNode(SantaPrefixNode *);
|
||||
|
||||
SantaPrefixNode *root_;
|
||||
|
||||
// Each node takes up ~2k, assuming MAXPATHLEN is 1024 max out at ~2MB.
|
||||
static const uint32_t kDefaultMaxNodes = MAXPATHLEN;
|
||||
uint32_t max_nodes_;
|
||||
uint32_t node_count_;
|
||||
|
||||
pthread_rwlock_t spt_lock_;
|
||||
std::mutex *spt_add_lock_;
|
||||
};
|
||||
|
||||
#endif /* SANTA__SANTA_DRIVER__SANTAPREFIXTREE_H */
|
||||
@@ -1,73 +0,0 @@
|
||||
/// Copyright 2018 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include "Source/common/SNTPrefixTree.h"
|
||||
|
||||
@interface SNTPrefixTreeTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTPrefixTreeTest
|
||||
|
||||
- (void)testAddAndHas {
|
||||
auto t = SNTPrefixTree();
|
||||
XCTAssertFalse(t.HasPrefix("/private/var/tmp/file1"));
|
||||
t.AddPrefix("/private/var/tmp/");
|
||||
XCTAssertTrue(t.HasPrefix("/private/var/tmp/file1"));
|
||||
}
|
||||
|
||||
- (void)testReset {
|
||||
auto t = SNTPrefixTree();
|
||||
t.AddPrefix("/private/var/tmp/");
|
||||
XCTAssertTrue(t.HasPrefix("/private/var/tmp/file1"));
|
||||
t.Reset();
|
||||
XCTAssertFalse(t.HasPrefix("/private/var/tmp/file1"));
|
||||
}
|
||||
|
||||
- (void)testThreading {
|
||||
uint32_t count = 4096;
|
||||
auto t = new SNTPrefixTree(count * (uint32_t)[NSUUID UUID].UUIDString.length);
|
||||
|
||||
NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
|
||||
for (int i = 0; i < count; ++i) {
|
||||
[UUIDs addObject:[NSUUID UUID].UUIDString];
|
||||
}
|
||||
|
||||
__block BOOL stop = NO;
|
||||
|
||||
// Create a bunch of background noise.
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
for (uint64_t i = 0; i < UINT64_MAX; ++i) {
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
t->HasPrefix([UUIDs[i % count] UTF8String]);
|
||||
});
|
||||
if (stop) return;
|
||||
}
|
||||
});
|
||||
|
||||
// Fill up the tree.
|
||||
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
|
||||
XCTAssertEqual(t->AddPrefix([UUIDs[i] UTF8String]), kIOReturnSuccess);
|
||||
});
|
||||
|
||||
// Make sure every leaf byte is found.
|
||||
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
|
||||
XCTAssertTrue(t->HasPrefix([UUIDs[i] UTF8String]));
|
||||
});
|
||||
|
||||
stop = YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -15,8 +15,8 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
|
||||
@class SNTRule;
|
||||
@class SNTStoredEvent;
|
||||
@@ -31,7 +31,7 @@
|
||||
/// Cache Ops
|
||||
///
|
||||
- (void)cacheCounts:(void (^)(uint64_t rootCache, uint64_t nonRootCache))reply;
|
||||
- (void)checkCacheForVnodeID:(santa_vnode_id_t)vnodeID withReply:(void (^)(santa_action_t))reply;
|
||||
- (void)checkCacheForVnodeID:(SantaVnode)vnodeID withReply:(void (^)(SNTAction))reply;
|
||||
|
||||
///
|
||||
/// Database ops
|
||||
@@ -63,6 +63,7 @@
|
||||
/// Config ops
|
||||
///
|
||||
- (void)watchdogInfo:(void (^)(uint64_t, uint64_t, double, double))reply;
|
||||
- (void)watchItemsState:(void (^)(BOOL, uint64_t, NSString *, NSString *, NSTimeInterval))reply;
|
||||
- (void)clientMode:(void (^)(SNTClientMode))reply;
|
||||
- (void)fullSyncLastSuccess:(void (^)(NSDate *))reply;
|
||||
- (void)ruleSyncLastSuccess:(void (^)(NSDate *))reply;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
#include "Source/common/BranchPrediction.h"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
40
Source/common/SantaVnode.h
Normal file
40
Source/common/SantaVnode.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON__SANTAVNODE_H
|
||||
#define SANTA__COMMON__SANTAVNODE_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
// Struct to manage vnode IDs
|
||||
typedef struct SantaVnode {
|
||||
dev_t fsid;
|
||||
ino_t fileid;
|
||||
|
||||
#ifdef __cplusplus
|
||||
bool operator==(const SantaVnode &rhs) const {
|
||||
return fsid == rhs.fsid && fileid == rhs.fileid;
|
||||
}
|
||||
|
||||
static inline SantaVnode VnodeForFile(const es_file_t *es_file) {
|
||||
return SantaVnode{
|
||||
.fsid = es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
#endif
|
||||
} SantaVnode;
|
||||
|
||||
#endif
|
||||
24
Source/common/SantaVnodeHash.h
Normal file
24
Source/common/SantaVnodeHash.h
Normal file
@@ -0,0 +1,24 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON__SANTAVNODEHASH_H
|
||||
#define SANTA__COMMON__SANTAVNODEHASH_H
|
||||
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/common/SantaVnode.h"
|
||||
|
||||
template <>
|
||||
uint64_t SantaCacheHasher<SantaVnode>(SantaVnode const &t);
|
||||
|
||||
#endif
|
||||
20
Source/common/SantaVnodeHash.mm
Normal file
20
Source/common/SantaVnodeHash.mm
Normal file
@@ -0,0 +1,20 @@
|
||||
/// Copyright 2022 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/common/SantaVnodeHash.h"
|
||||
|
||||
template <>
|
||||
uint64_t SantaCacheHasher<SantaVnode>(SantaVnode const &t) {
|
||||
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
|
||||
}
|
||||
41
Source/common/SystemResources.h
Normal file
41
Source/common/SystemResources.h
Normal file
@@ -0,0 +1,41 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON__SYSTEMRESOURCES_H
|
||||
#define SANTA__COMMON__SYSTEMRESOURCES_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <sys/cdefs.h>
|
||||
#include <sys/proc_info.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
struct SantaTaskInfo {
|
||||
uint64_t virtual_size;
|
||||
uint64_t resident_size;
|
||||
uint64_t total_user_nanos;
|
||||
uint64_t total_system_nanos;
|
||||
};
|
||||
|
||||
// Convert mach absolute time to nanoseconds
|
||||
uint64_t MachTimeToNanos(uint64_t mach_time);
|
||||
|
||||
// Convert nanoseconds to mach absolute time
|
||||
uint64_t NanosToMachTime(uint64_t nanos);
|
||||
|
||||
// Get the result of proc_pidinfo with the PROC_PIDTASKINFO flavor
|
||||
std::optional<SantaTaskInfo> GetTaskInfo();
|
||||
|
||||
#endif
|
||||
68
Source/common/SystemResources.mm
Normal file
68
Source/common/SystemResources.mm
Normal file
@@ -0,0 +1,68 @@
|
||||
/// Copyright 2022 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/common/SystemResources.h"
|
||||
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <libproc.h>
|
||||
#include <mach/kern_return.h>
|
||||
#include <unistd.h>
|
||||
#include <optional>
|
||||
|
||||
#include "Source/common/SNTLogging.h"
|
||||
|
||||
static mach_timebase_info_data_t GetTimebase() {
|
||||
static dispatch_once_t once_token;
|
||||
static mach_timebase_info_data_t timebase;
|
||||
|
||||
dispatch_once(&once_token, ^{
|
||||
if (mach_timebase_info(&timebase) != KERN_SUCCESS) {
|
||||
// This shouldn't fail. Assume transitory and exit the program.
|
||||
// Hopefully fixes itself on restart...
|
||||
LOGE(@"Failed to get timebase info. Exiting.");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
});
|
||||
|
||||
return timebase;
|
||||
}
|
||||
|
||||
uint64_t MachTimeToNanos(uint64_t mach_time) {
|
||||
mach_timebase_info_data_t timebase = GetTimebase();
|
||||
|
||||
return mach_time * timebase.numer / timebase.denom;
|
||||
}
|
||||
|
||||
uint64_t NanosToMachTime(uint64_t nanos) {
|
||||
mach_timebase_info_data_t timebase = GetTimebase();
|
||||
|
||||
return nanos * timebase.denom / timebase.numer;
|
||||
}
|
||||
|
||||
std::optional<SantaTaskInfo> GetTaskInfo() {
|
||||
struct proc_taskinfo pti;
|
||||
|
||||
if (proc_pidinfo(getpid(), PROC_PIDTASKINFO, 0, &pti, PROC_PIDTASKINFO_SIZE) <
|
||||
PROC_PIDTASKINFO_SIZE) {
|
||||
LOGW(@"Unable to get system resource information");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return SantaTaskInfo{
|
||||
.virtual_size = pti.pti_virtual_size,
|
||||
.resident_size = pti.pti_resident_size,
|
||||
.total_user_nanos = MachTimeToNanos(pti.pti_total_user),
|
||||
.total_system_nanos = MachTimeToNanos(pti.pti_total_system),
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <mach/mach_time.h>
|
||||
#include <time.h>
|
||||
#include <uuid/uuid.h>
|
||||
#include "Source/common/SystemResources.h"
|
||||
|
||||
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver) {
|
||||
return audit_token_t{
|
||||
@@ -87,21 +88,13 @@ es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t par
|
||||
}
|
||||
|
||||
static uint64_t AddMillisToMachTime(uint64_t ms, uint64_t machTime) {
|
||||
static dispatch_once_t onceToken;
|
||||
static mach_timebase_info_data_t timebase;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
mach_timebase_info(&timebase);
|
||||
});
|
||||
|
||||
// Convert given machTime to nanoseconds
|
||||
uint64_t nanoTime = machTime * timebase.numer / timebase.denom;
|
||||
uint64_t nanoTime = MachTimeToNanos(machTime);
|
||||
|
||||
// Add the ms offset
|
||||
nanoTime += (ms * NSEC_PER_MSEC);
|
||||
|
||||
// Convert back to machTime
|
||||
return nanoTime * timebase.denom / timebase.numer;
|
||||
return NanosToMachTime(nanoTime);
|
||||
}
|
||||
|
||||
uint32_t MaxSupportedESMessageVersionForCurrentOS() {
|
||||
|
||||
24
Source/common/Unit.h
Normal file
24
Source/common/Unit.h
Normal file
@@ -0,0 +1,24 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON__UNIT_H
|
||||
#define SANTA__COMMON__UNIT_H
|
||||
|
||||
namespace santa::common {
|
||||
|
||||
struct Unit {};
|
||||
|
||||
} // namespace santa::common
|
||||
|
||||
#endif
|
||||
@@ -464,6 +464,45 @@ message Allowlist {
|
||||
optional FileInfo target = 2;
|
||||
}
|
||||
|
||||
// Information about access to a watched path
|
||||
message FileAccess {
|
||||
// The process that attempted to access the watched path
|
||||
optional ProcessInfo instigator = 1;
|
||||
|
||||
// The path that was accessed
|
||||
optional FileInfoLight target = 2;
|
||||
|
||||
// The version of the policy when the decision was made
|
||||
optional string policy_version = 3;
|
||||
|
||||
// The name of the specific policy that triggered this log
|
||||
optional string policy_name = 4;
|
||||
|
||||
// The event type that attempted to access the watched path
|
||||
enum AccessType {
|
||||
ACCESS_TYPE_UNKNOWN = 0;
|
||||
ACCESS_TYPE_OPEN = 1;
|
||||
ACCESS_TYPE_RENAME = 2;
|
||||
ACCESS_TYPE_UNLINK = 3;
|
||||
ACCESS_TYPE_LINK = 4;
|
||||
ACCESS_TYPE_CLONE = 5;
|
||||
ACCESS_TYPE_EXCHANGEDATA = 6;
|
||||
ACCESS_TYPE_COPYFILE = 7;
|
||||
ACCESS_TYPE_CREATE = 8;
|
||||
ACCESS_TYPE_TRUNCATE = 9;
|
||||
}
|
||||
optional AccessType access_type = 5;
|
||||
|
||||
// Whether the operation was allowed or denied and why
|
||||
enum PolicyDecision {
|
||||
POLICY_DECISION_UNKNOWN = 0;
|
||||
POLICY_DECISION_DENIED = 1;
|
||||
POLICY_DECISION_DENIED_INVALID_SIGNATURE = 2;
|
||||
POLICY_DECISION_ALLOWED_AUDIT_ONLY = 3;
|
||||
}
|
||||
optional PolicyDecision policy_decision = 6;
|
||||
}
|
||||
|
||||
// A message encapsulating a single event
|
||||
message SantaMessage {
|
||||
// Machine ID of the host emitting this log
|
||||
@@ -489,6 +528,7 @@ message SantaMessage {
|
||||
Disk disk = 18;
|
||||
Bundle bundle = 19;
|
||||
Allowlist allowlist = 20;
|
||||
FileAccess file_access = 21;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ macos_application(
|
||||
"//conditions:default": None,
|
||||
}),
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "10.15",
|
||||
minimum_os_version = "11.0",
|
||||
provisioning_profile = select({
|
||||
"//:adhoc_build": None,
|
||||
"//conditions:default": "//profiles:santa_dev",
|
||||
|
||||
@@ -92,10 +92,16 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
}
|
||||
|
||||
- (void)queueMessage:(SNTMessageWindowController *)pendingMsg {
|
||||
NSString *messageHash = [pendingMsg messageHash];
|
||||
// Post a distributed notification, regardless of queue state.
|
||||
[self postDistributedNotification:pendingMsg];
|
||||
|
||||
// If GUI is in silent mode or if there's already a notification queued for
|
||||
// this message, don't do anything else.
|
||||
if ([SNTConfigurator configurator].enableSilentMode) return;
|
||||
if ([self notificationAlreadyQueued:pendingMsg]) return;
|
||||
|
||||
// See if this message is silenced.
|
||||
// See if this message has been user-silenced.
|
||||
NSString *messageHash = [pendingMsg messageHash];
|
||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||
NSDate *silenceDate = [ud objectForKey:silencedNotificationsKey][messageHash];
|
||||
if ([silenceDate isKindOfClass:[NSDate class]]) {
|
||||
@@ -114,7 +120,6 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
|
||||
pendingMsg.delegate = self;
|
||||
[self.pendingNotifications addObject:pendingMsg];
|
||||
[self postDistributedNotification:pendingMsg];
|
||||
|
||||
if (!self.currentWindowController) {
|
||||
[self showQueuedWindow];
|
||||
@@ -315,8 +320,6 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
}
|
||||
|
||||
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message {
|
||||
if ([SNTConfigurator configurator].enableSilentMode) return;
|
||||
|
||||
if (!event) {
|
||||
LOGI(@"Error: Missing event object in message received from daemon!");
|
||||
return;
|
||||
@@ -329,8 +332,6 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
}
|
||||
|
||||
- (void)postUSBBlockNotification:(SNTDeviceEvent *)event withCustomMessage:(NSString *)message {
|
||||
if ([SNTConfigurator configurator].enableSilentMode) return;
|
||||
|
||||
if (!event) {
|
||||
LOGI(@"Error: Missing event object in message received from daemon!");
|
||||
return;
|
||||
|
||||
@@ -62,28 +62,24 @@ int main(int argc, const char *argv[]) {
|
||||
sysxOperation = @(2);
|
||||
}
|
||||
if (sysxOperation) {
|
||||
if (@available(macOS 10.15, *)) {
|
||||
NSString *e = [SNTXPCControlInterface systemExtensionID];
|
||||
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
|
||||
OSSystemExtensionRequest *req;
|
||||
if (sysxOperation.intValue == 1) {
|
||||
NSLog(@"Requesting SystemExtension activation");
|
||||
req = [OSSystemExtensionRequest activationRequestForExtension:e queue:q];
|
||||
} else if (sysxOperation.intValue == 2) {
|
||||
NSLog(@"Requesting SystemExtension deactivation");
|
||||
req = [OSSystemExtensionRequest deactivationRequestForExtension:e queue:q];
|
||||
}
|
||||
if (req) {
|
||||
SNTSystemExtensionDelegate *ed = [[SNTSystemExtensionDelegate alloc] init];
|
||||
req.delegate = ed;
|
||||
[[OSSystemExtensionManager sharedManager] submitRequest:req];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 60), q, ^{
|
||||
exit(1);
|
||||
});
|
||||
[[NSRunLoop mainRunLoop] run];
|
||||
}
|
||||
} else {
|
||||
exit(1);
|
||||
NSString *e = [SNTXPCControlInterface systemExtensionID];
|
||||
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
|
||||
OSSystemExtensionRequest *req;
|
||||
if (sysxOperation.intValue == 1) {
|
||||
NSLog(@"Requesting SystemExtension activation");
|
||||
req = [OSSystemExtensionRequest activationRequestForExtension:e queue:q];
|
||||
} else if (sysxOperation.intValue == 2) {
|
||||
NSLog(@"Requesting SystemExtension deactivation");
|
||||
req = [OSSystemExtensionRequest deactivationRequestForExtension:e queue:q];
|
||||
}
|
||||
if (req) {
|
||||
SNTSystemExtensionDelegate *ed = [[SNTSystemExtensionDelegate alloc] init];
|
||||
req.delegate = ed;
|
||||
[[OSSystemExtensionManager sharedManager] submitRequest:req];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 60), q, ^{
|
||||
exit(1);
|
||||
});
|
||||
[[NSRunLoop mainRunLoop] run];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ macos_command_line_application(
|
||||
"--options library,kill,runtime",
|
||||
],
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "10.15",
|
||||
minimum_os_version = "11.0",
|
||||
provisioning_profile = select({
|
||||
"//:adhoc_build": None,
|
||||
"//conditions:default": "//profiles:santa_dev",
|
||||
|
||||
@@ -59,7 +59,6 @@ objc_library(
|
||||
":SNTCommandPrintLog",
|
||||
":santactl_cmd",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommon",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDropRootPrivs",
|
||||
@@ -91,7 +90,7 @@ macos_command_line_application(
|
||||
"--options library,kill,runtime",
|
||||
],
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "10.15",
|
||||
minimum_os_version = "11.0",
|
||||
provisioning_profile = select({
|
||||
"//:adhoc_build": None,
|
||||
"//conditions:default": "//profiles:santa_dev",
|
||||
|
||||
@@ -49,30 +49,30 @@ REGISTER_COMMAND_NAME(@"checkcache")
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
santa_vnode_id_t vnodeID = [self vnodeIDForFile:arguments.firstObject];
|
||||
SantaVnode vnodeID = [self vnodeIDForFile:arguments.firstObject];
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
checkCacheForVnodeID:vnodeID
|
||||
withReply:^(santa_action_t action) {
|
||||
if (action == ACTION_RESPOND_ALLOW) {
|
||||
withReply:^(SNTAction action) {
|
||||
if (action == SNTActionRespondAllow) {
|
||||
LOGI(@"File exists in [allowlist] kernel cache");
|
||||
exit(0);
|
||||
} else if (action == ACTION_RESPOND_DENY) {
|
||||
} else if (action == SNTActionRespondDeny) {
|
||||
LOGI(@"File exists in [blocklist] kernel cache");
|
||||
exit(0);
|
||||
} else if (action == ACTION_RESPOND_ALLOW_COMPILER) {
|
||||
} else if (action == SNTActionRespondAllowCompiler) {
|
||||
LOGI(@"File exists in [allowlist compiler] kernel cache");
|
||||
exit(0);
|
||||
} else if (action == ACTION_UNSET) {
|
||||
} else if (action == SNTActionUnset) {
|
||||
LOGE(@"File does not exist in cache");
|
||||
exit(1);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (santa_vnode_id_t)vnodeIDForFile:(NSString *)path {
|
||||
- (SantaVnode)vnodeIDForFile:(NSString *)path {
|
||||
struct stat fstat = {};
|
||||
stat(path.fileSystemRepresentation, &fstat);
|
||||
santa_vnode_id_t ret = {.fsid = fstat.st_dev, .fileid = fstat.st_ino};
|
||||
SantaVnode ret = {.fsid = fstat.st_dev, .fileid = fstat.st_ino};
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,26 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
__block BOOL watchItemsEnabled = NO;
|
||||
__block uint64_t watchItemsRuleCount = 0;
|
||||
__block NSString *watchItemsPolicyVersion = nil;
|
||||
__block NSString *watchItemsConfigPath = nil;
|
||||
__block NSTimeInterval watchItemsLastUpdateEpoch = 0;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
watchItemsState:^(BOOL enabled, uint64_t ruleCount, NSString *policyVersion,
|
||||
NSString *configPath, NSTimeInterval lastUpdateEpoch) {
|
||||
watchItemsEnabled = enabled;
|
||||
if (enabled) {
|
||||
watchItemsRuleCount = ruleCount;
|
||||
watchItemsPolicyVersion = policyVersion;
|
||||
watchItemsConfigPath = configPath;
|
||||
watchItemsLastUpdateEpoch = lastUpdateEpoch;
|
||||
}
|
||||
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Wait a maximum of 5s for stats collected from daemon to arrive.
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
|
||||
fprintf(stderr, "Failed to retrieve some stats from daemon\n\n");
|
||||
@@ -170,6 +190,10 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
NSString *ruleSyncLastSuccessStr =
|
||||
[dateFormatter stringFromDate:ruleSyncLastSuccess] ?: fullSyncLastSuccessStr;
|
||||
|
||||
NSString *watchItemsLastUpdateStr =
|
||||
[dateFormatter stringFromDate:[NSDate dateWithTimeIntervalSince1970:watchItemsLastUpdateEpoch]]
|
||||
?: @"Never";
|
||||
|
||||
NSString *syncURLStr = configurator.syncBaseURL.absoluteString;
|
||||
|
||||
BOOL exportMetrics = configurator.exportMetrics;
|
||||
@@ -212,6 +236,22 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
},
|
||||
} mutableCopy];
|
||||
|
||||
NSDictionary *watchItems;
|
||||
if (watchItemsEnabled) {
|
||||
watchItems = @{
|
||||
@"enabled" : @(watchItemsEnabled),
|
||||
@"rule_count" : @(watchItemsRuleCount),
|
||||
@"policy_version" : watchItemsPolicyVersion,
|
||||
@"config_path" : watchItemsConfigPath,
|
||||
@"last_policy_update" : watchItemsLastUpdateStr ?: @"null",
|
||||
};
|
||||
} else {
|
||||
watchItems = @{
|
||||
@"enabled" : @(watchItemsEnabled),
|
||||
};
|
||||
}
|
||||
stats[@"watch_items"] = watchItems;
|
||||
|
||||
stats[@"cache"] = @{
|
||||
@"root_cache_count" : @(rootCacheCount),
|
||||
@"non_root_cache_count" : @(nonRootCacheCount),
|
||||
@@ -251,6 +291,15 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %lld\n", "Rules", staticRuleCount);
|
||||
}
|
||||
|
||||
printf(">>> Watch Items\n");
|
||||
printf(" %-25s | %s\n", "Enabled", (watchItemsEnabled ? "Yes" : "No"));
|
||||
if (watchItemsEnabled) {
|
||||
printf(" %-25s | %s\n", "Policy Version", watchItemsPolicyVersion.UTF8String);
|
||||
printf(" %-25s | %llu\n", "Rule Count", watchItemsRuleCount);
|
||||
printf(" %-25s | %s\n", "Config Path", watchItemsConfigPath.UTF8String);
|
||||
printf(" %-25s | %s\n", "Last Policy Update", watchItemsLastUpdateStr.UTF8String);
|
||||
}
|
||||
|
||||
if (syncURLStr) {
|
||||
printf(">>> Sync Info\n");
|
||||
printf(" %-25s | %s\n", "Sync Server", [syncURLStr UTF8String]);
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
|
||||
@@ -23,6 +23,7 @@ objc_library(
|
||||
hdrs = ["DataLayer/SNTRuleTable.h"],
|
||||
deps = [
|
||||
":SNTDatabaseTable",
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -34,6 +35,37 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "WatchItemPolicy",
|
||||
hdrs = ["DataLayer/WatchItemPolicy.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "WatchItems",
|
||||
srcs = ["DataLayer/WatchItems.mm"],
|
||||
hdrs = ["DataLayer/WatchItems.h"],
|
||||
deps = [
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:Unit",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "WatchItemsTest",
|
||||
srcs = ["DataLayer/WatchItemsTest.mm"],
|
||||
deps = [
|
||||
":WatchItemPolicy",
|
||||
":WatchItems",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:TestUtils",
|
||||
"//Source/common:Unit",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTEventTable",
|
||||
srcs = ["DataLayer/SNTEventTable.m"],
|
||||
@@ -63,19 +95,20 @@ objc_library(
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
"//Source/common:SNTCommon",
|
||||
":WatchItemPolicy",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTApplicationCoreMetrics",
|
||||
srcs = ["SNTApplicationCoreMetrics.m"],
|
||||
srcs = ["SNTApplicationCoreMetrics.mm"],
|
||||
hdrs = ["SNTApplicationCoreMetrics.h"],
|
||||
deps = [
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTSystemInfo",
|
||||
"//Source/common:SystemResources",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -118,7 +151,6 @@ objc_library(
|
||||
":SNTDecisionCache",
|
||||
":SNTRuleTable",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommon",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
@@ -161,7 +193,6 @@ objc_library(
|
||||
hdrs = ["SNTPolicyProcessor.h"],
|
||||
deps = [
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommon",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTFileInfo",
|
||||
@@ -186,9 +217,9 @@ objc_library(
|
||||
":SNTPolicyProcessor",
|
||||
":SNTRuleTable",
|
||||
":SNTSyncdQueue",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:SNTBlockMessage",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommon",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDropRootPrivs",
|
||||
@@ -197,6 +228,7 @@ objc_library(
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SantaVnode",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
)
|
||||
@@ -210,6 +242,7 @@ objc_library(
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":WatchItemPolicy",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -224,9 +257,11 @@ objc_library(
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":SNTEndpointSecurityClientBase",
|
||||
"//Source/common:SNTCommon",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SystemResources",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -245,8 +280,9 @@ objc_library(
|
||||
":SNTCompilerController",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTPrefixTree",
|
||||
"//Source/common:Unit",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -261,6 +297,7 @@ objc_library(
|
||||
":Metrics",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:SNTLogging",
|
||||
],
|
||||
)
|
||||
@@ -280,10 +317,39 @@ objc_library(
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
":SNTExecutionController",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTEndpointSecurityFileAccessAuthorizer",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.mm"],
|
||||
hdrs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.h"],
|
||||
deps = [
|
||||
":EndpointSecurityAPI",
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
":EndpointSecurityEnricher",
|
||||
":EndpointSecurityLogger",
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
":WatchItemPolicy",
|
||||
":WatchItems",
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTEndpointSecurityDeviceManager",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityDeviceManager.mm"],
|
||||
@@ -308,9 +374,11 @@ objc_library(
|
||||
deps = [
|
||||
":EndpointSecurityAPI",
|
||||
":EndpointSecurityClient",
|
||||
"//Source/common:SNTCommon",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -341,6 +409,7 @@ objc_library(
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
":EndpointSecurityMessage",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
],
|
||||
)
|
||||
@@ -434,6 +503,7 @@ objc_library(
|
||||
hdrs = ["Logs/EndpointSecurity/Writers/File.h"],
|
||||
deps = [
|
||||
":EndpointSecurityWriter",
|
||||
"//Source/common:BranchPrediction",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -492,6 +562,7 @@ objc_library(
|
||||
hdrs = ["EventProviders/EndpointSecurity/Message.h"],
|
||||
deps = [
|
||||
":EndpointSecurityClient",
|
||||
":WatchItemPolicy",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -507,6 +578,8 @@ objc_library(
|
||||
deps = [
|
||||
":EndpointSecurityClient",
|
||||
":EndpointSecurityMessage",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:Platform",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -523,6 +596,7 @@ objc_library(
|
||||
":SNTPolicyProcessor",
|
||||
":SNTRuleTable",
|
||||
":SNTSyncdQueue",
|
||||
":WatchItems",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -564,20 +638,24 @@ objc_library(
|
||||
":Metrics",
|
||||
":SNTCompilerController",
|
||||
":SNTDaemonControlController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityAuthorizer",
|
||||
":SNTEndpointSecurityDeviceManager",
|
||||
":SNTEndpointSecurityFileAccessAuthorizer",
|
||||
":SNTEndpointSecurityRecorder",
|
||||
":SNTEndpointSecurityTamperResistance",
|
||||
":SNTExecutionController",
|
||||
":SNTNotificationQueue",
|
||||
":SNTSyncdQueue",
|
||||
":WatchItems",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTKVOManager",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTPrefixTree",
|
||||
"//Source/common:SNTXPCNotifierInterface",
|
||||
"//Source/common:SNTXPCSyncServiceInterface",
|
||||
"//Source/common:Unit",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
@@ -599,12 +677,14 @@ objc_library(
|
||||
":SNTNotificationQueue",
|
||||
":SNTRuleTable",
|
||||
":SNTSyncdQueue",
|
||||
":WatchItems",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTPrefixTree",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"//Source/common:SNTXPCUnprivilegedControlInterface",
|
||||
"//Source/common:Unit",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
@@ -627,6 +707,7 @@ objc_library(
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"//Source/common:SystemResources",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -646,7 +727,7 @@ macos_bundle(
|
||||
}),
|
||||
infoplists = ["Info.plist"],
|
||||
linkopts = ["-execute"],
|
||||
minimum_os_version = "10.15",
|
||||
minimum_os_version = "11.0",
|
||||
provisioning_profile = select({
|
||||
"//:adhoc_build": None,
|
||||
"//conditions:default": "//profiles:daemon_dev",
|
||||
@@ -671,6 +752,18 @@ objc_library(
|
||||
":EndpointSecurityAPI",
|
||||
":EndpointSecurityClient",
|
||||
":EndpointSecurityMessage",
|
||||
":WatchItemPolicy",
|
||||
"@com_google_googletest//:gtest",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "MockLogger",
|
||||
testonly = 1,
|
||||
hdrs = ["Logs/EndpointSecurity/MockLogger.h"],
|
||||
deps = [
|
||||
":EndpointSecurityLogger",
|
||||
":EndpointSecurityMessage",
|
||||
"@com_google_googletest//:gtest",
|
||||
],
|
||||
)
|
||||
@@ -707,6 +800,7 @@ santa_unit_test(
|
||||
"EndpointSecurity",
|
||||
],
|
||||
deps = [
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -725,7 +819,7 @@ santa_unit_test(
|
||||
data = [
|
||||
"//Source/santad/testdata:binaryrules_testdata",
|
||||
],
|
||||
minimum_os_version = "10.15",
|
||||
minimum_os_version = "11.0",
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
"EndpointSecurity",
|
||||
@@ -753,9 +847,9 @@ santa_unit_test(
|
||||
santa_unit_test(
|
||||
name = "SNTApplicationCoreMetricsTest",
|
||||
srcs = [
|
||||
"SNTApplicationCoreMetricsTest.m",
|
||||
"SNTApplicationCoreMetricsTest.mm",
|
||||
],
|
||||
minimum_os_version = "10.15",
|
||||
minimum_os_version = "11.0",
|
||||
deps = [
|
||||
":SNTApplicationCoreMetrics",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
@@ -852,7 +946,7 @@ santa_unit_test(
|
||||
deps = [
|
||||
":AuthResultCache",
|
||||
":MockEndpointSecurityAPI",
|
||||
"//Source/common:SNTCommon",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
@@ -982,7 +1076,6 @@ santa_unit_test(
|
||||
":SNTDecisionCache",
|
||||
":SNTRuleTable",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommon",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:TestUtils",
|
||||
@@ -1004,6 +1097,7 @@ santa_unit_test(
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityClient",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
@@ -1026,7 +1120,6 @@ santa_unit_test(
|
||||
":SNTExecutionController",
|
||||
":SNTRuleTable",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommon",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTFileInfo",
|
||||
@@ -1054,12 +1147,39 @@ santa_unit_test(
|
||||
":SNTCompilerController",
|
||||
":SNTEndpointSecurityAuthorizer",
|
||||
":SNTExecutionController",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTEndpointSecurityFileAccessAuthorizerTest",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizerTest.mm"],
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityLogger",
|
||||
":EndpointSecurityMessage",
|
||||
":MockEndpointSecurityAPI",
|
||||
":MockLogger",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityFileAccessAuthorizer",
|
||||
":WatchItemPolicy",
|
||||
":WatchItems",
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTEndpointSecurityTamperResistanceTest",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityTamperResistanceTest.mm"],
|
||||
@@ -1072,6 +1192,7 @@ santa_unit_test(
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityTamperResistance",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
@@ -1096,7 +1217,9 @@ santa_unit_test(
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTCompilerController",
|
||||
":SNTEndpointSecurityRecorder",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:TestUtils",
|
||||
"//Source/common:Unit",
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
],
|
||||
@@ -1167,12 +1290,14 @@ test_suite(
|
||||
":SNTEndpointSecurityAuthorizerTest",
|
||||
":SNTEndpointSecurityClientTest",
|
||||
":SNTEndpointSecurityDeviceManagerTest",
|
||||
":SNTEndpointSecurityFileAccessAuthorizerTest",
|
||||
":SNTEndpointSecurityRecorderTest",
|
||||
":SNTEndpointSecurityTamperResistanceTest",
|
||||
":SNTEventTableTest",
|
||||
":SNTExecutionControllerTest",
|
||||
":SNTRuleTableTest",
|
||||
":SantadTest",
|
||||
":WatchItemsTest",
|
||||
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool_test",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
|
||||
#import "Source/common/Platform.h"
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
@@ -36,7 +37,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
// instead we use the following preprocessor macros to conditionally compile these API. The
|
||||
// drawback here is that if a pre-macOS 12 SDK is used to build Santa and it is then deployed
|
||||
// on macOS 12 or later, the dynamic mute set will not be computed.
|
||||
#if defined(MAC_OS_VERSION_12_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0
|
||||
#if HAVE_MACOS_12
|
||||
// Create a temporary ES client in order to grab the default set of muted paths.
|
||||
// TODO(mlw): Reorganize this code so that a temporary ES client doesn't need to be created
|
||||
es_client_t *client = NULL;
|
||||
|
||||
95
Source/santad/DataLayer/WatchItemPolicy.h
Normal file
95
Source/santad/DataLayer/WatchItemPolicy.h
Normal file
@@ -0,0 +1,95 @@
|
||||
/// Copyright 2022 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__DATALAYER_WATCHITEMPOLICY_H
|
||||
#define SANTA__SANTAD__DATALAYER_WATCHITEMPOLICY_H
|
||||
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace santa::santad::data_layer {
|
||||
|
||||
enum class WatchItemPathType {
|
||||
kPrefix,
|
||||
kLiteral,
|
||||
};
|
||||
|
||||
static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType =
|
||||
WatchItemPathType::kLiteral;
|
||||
static constexpr bool kWatchItemPolicyDefaultAllowReadAccess = false;
|
||||
static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
|
||||
|
||||
struct WatchItemPolicy {
|
||||
struct Process {
|
||||
Process(std::string bp, std::string sid, std::string ti,
|
||||
std::vector<uint8_t> cdh, std::string ch)
|
||||
: binary_path(bp),
|
||||
signing_id(sid),
|
||||
team_id(ti),
|
||||
cdhash(std::move(cdh)),
|
||||
certificate_sha256(ch) {}
|
||||
|
||||
bool operator==(const Process &other) const {
|
||||
return binary_path == other.binary_path &&
|
||||
signing_id == other.signing_id && team_id == other.team_id &&
|
||||
cdhash == other.cdhash &&
|
||||
certificate_sha256 == other.certificate_sha256;
|
||||
}
|
||||
|
||||
bool operator!=(const Process &other) const { return !(*this == other); }
|
||||
|
||||
std::string binary_path;
|
||||
std::string signing_id;
|
||||
std::string team_id;
|
||||
std::vector<uint8_t> cdhash;
|
||||
std::string certificate_sha256;
|
||||
};
|
||||
|
||||
WatchItemPolicy(std::string_view n, std::string_view p,
|
||||
WatchItemPathType pt = kWatchItemPolicyDefaultPathType,
|
||||
bool ara = kWatchItemPolicyDefaultAllowReadAccess,
|
||||
bool ao = kWatchItemPolicyDefaultAuditOnly,
|
||||
std::vector<Process> procs = {})
|
||||
: name(n),
|
||||
path(p),
|
||||
path_type(pt),
|
||||
allow_read_access(ara),
|
||||
audit_only(ao),
|
||||
processes(std::move(procs)) {}
|
||||
|
||||
bool operator==(const WatchItemPolicy &other) const {
|
||||
return name == other.name && path == other.path &&
|
||||
path_type == other.path_type &&
|
||||
allow_read_access == other.allow_read_access &&
|
||||
audit_only == other.audit_only && processes == other.processes;
|
||||
}
|
||||
|
||||
bool operator!=(const WatchItemPolicy &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
std::string name;
|
||||
std::string path;
|
||||
WatchItemPathType path_type;
|
||||
bool allow_read_access;
|
||||
bool audit_only;
|
||||
std::vector<Process> processes;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
|
||||
#endif
|
||||
118
Source/santad/DataLayer/WatchItems.h
Normal file
118
Source/santad/DataLayer/WatchItems.h
Normal file
@@ -0,0 +1,118 @@
|
||||
/// Copyright 2022 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__DATALAYER_WATCHITEMS_H
|
||||
#define SANTA__SANTAD__DATALAYER_WATCHITEMS_H
|
||||
|
||||
#include <CommonCrypto/CommonDigest.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/common/PrefixTree.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
|
||||
|
||||
extern NSString *const kWatchItemConfigKeyVersion;
|
||||
extern NSString *const kWatchItemConfigKeyWatchItems;
|
||||
extern NSString *const kWatchItemConfigKeyPaths;
|
||||
extern NSString *const kWatchItemConfigKeyPathsPath;
|
||||
extern NSString *const kWatchItemConfigKeyPathsIsPrefix;
|
||||
extern NSString *const kWatchItemConfigKeyOptions;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsAllowReadAccess;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsAuditOnly;
|
||||
extern NSString *const kWatchItemConfigKeyProcesses;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesBinaryPath;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesCertificateSha256;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesSigningID;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesTeamID;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesCDHash;
|
||||
|
||||
// Forward declarations
|
||||
namespace santa::santad::data_layer {
|
||||
class WatchItemsPeer;
|
||||
}
|
||||
|
||||
namespace santa::santad::data_layer {
|
||||
|
||||
struct WatchItemsState {
|
||||
uint64_t rule_count;
|
||||
NSString *policy_version;
|
||||
NSString *config_path;
|
||||
NSTimeInterval last_config_load_epoch;
|
||||
};
|
||||
|
||||
class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
public:
|
||||
using VersionAndPolicies =
|
||||
std::pair<std::string, std::vector<std::optional<std::shared_ptr<WatchItemPolicy>>>>;
|
||||
using WatchItemsTree = santa::common::PrefixTree<std::shared_ptr<WatchItemPolicy>>;
|
||||
|
||||
// Factory
|
||||
static std::shared_ptr<WatchItems> Create(NSString *config_path,
|
||||
uint64_t reapply_config_frequency_secs);
|
||||
|
||||
WatchItems(NSString *config_path_, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
void (^periodic_task_complete_f)(void) = nullptr);
|
||||
~WatchItems();
|
||||
|
||||
void BeginPeriodicTask();
|
||||
|
||||
void RegisterClient(id<SNTEndpointSecurityDynamicEventHandler> client);
|
||||
|
||||
void SetConfigPath(NSString *config_path);
|
||||
VersionAndPolicies FindPolciesForPaths(const std::vector<std::string_view> &paths);
|
||||
|
||||
std::optional<WatchItemsState> State();
|
||||
|
||||
friend class santa::santad::data_layer::WatchItemsPeer;
|
||||
|
||||
private:
|
||||
NSDictionary *ReadConfig();
|
||||
NSDictionary *ReadConfigLocked() ABSL_SHARED_LOCKS_REQUIRED(lock_);
|
||||
void ReloadConfig(NSDictionary *new_config);
|
||||
void UpdateCurrentState(std::unique_ptr<WatchItemsTree> new_tree,
|
||||
std::set<std::pair<std::string, WatchItemPathType>> &&new_monitored_paths,
|
||||
NSDictionary *new_config);
|
||||
bool BuildPolicyTree(const std::vector<std::shared_ptr<WatchItemPolicy>> &watch_items,
|
||||
WatchItemsTree &tree,
|
||||
std::set<std::pair<std::string, WatchItemPathType>> &paths);
|
||||
|
||||
NSString *config_path_;
|
||||
dispatch_queue_t q_;
|
||||
dispatch_source_t timer_source_;
|
||||
void (^periodic_task_complete_f_)(void);
|
||||
|
||||
absl::Mutex lock_;
|
||||
|
||||
std::unique_ptr<WatchItemsTree> watch_items_ ABSL_GUARDED_BY(lock_);
|
||||
NSDictionary *current_config_ ABSL_GUARDED_BY(lock_);
|
||||
NSTimeInterval last_update_time_ ABSL_GUARDED_BY(lock_);
|
||||
std::set<std::pair<std::string, WatchItemPathType>> currently_monitored_paths_
|
||||
ABSL_GUARDED_BY(lock_);
|
||||
std::string policy_version_ ABSL_GUARDED_BY(lock_);
|
||||
std::set<id<SNTEndpointSecurityDynamicEventHandler>> registerd_clients_ ABSL_GUARDED_BY(lock_);
|
||||
bool periodic_task_started_ = false;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
|
||||
#endif
|
||||
702
Source/santad/DataLayer/WatchItems.mm
Normal file
702
Source/santad/DataLayer/WatchItems.mm
Normal file
@@ -0,0 +1,702 @@
|
||||
/// Copyright 2022 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/DataLayer/WatchItems.h"
|
||||
|
||||
#include <CommonCrypto/CommonDigest.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#include <ctype.h>
|
||||
#include <glob.h>
|
||||
#include <objc/NSObjCRuntime.h>
|
||||
#include <sys/syslimits.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#import "Source/common/PrefixTree.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/Unit.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
|
||||
NSString *const kWatchItemConfigKeyVersion = @"Version";
|
||||
NSString *const kWatchItemConfigKeyWatchItems = @"WatchItems";
|
||||
NSString *const kWatchItemConfigKeyPaths = @"Paths";
|
||||
NSString *const kWatchItemConfigKeyPathsPath = @"Path";
|
||||
NSString *const kWatchItemConfigKeyPathsIsPrefix = @"IsPrefix";
|
||||
NSString *const kWatchItemConfigKeyOptions = @"Options";
|
||||
NSString *const kWatchItemConfigKeyOptionsAllowReadAccess = @"AllowReadAccess";
|
||||
NSString *const kWatchItemConfigKeyOptionsAuditOnly = @"AuditOnly";
|
||||
NSString *const kWatchItemConfigKeyProcesses = @"Processes";
|
||||
NSString *const kWatchItemConfigKeyProcessesBinaryPath = @"BinaryPath";
|
||||
NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha256";
|
||||
NSString *const kWatchItemConfigKeyProcessesSigningID = @"SigningID";
|
||||
NSString *const kWatchItemConfigKeyProcessesTeamID = @"TeamID";
|
||||
NSString *const kWatchItemConfigKeyProcessesCDHash = @"CDHash";
|
||||
|
||||
// https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/
|
||||
static constexpr NSUInteger kMaxTeamIDLength = 10;
|
||||
|
||||
// Semi-arbitrary upper bound.
|
||||
static constexpr NSUInteger kMaxSigningIDLength = 512;
|
||||
|
||||
// Semi-arbitrary minimum allowed reapplication frequency.
|
||||
// Goal is to prevent a configuration setting that would cause too much
|
||||
// churn rebuilding glob paths based on the state of the filesystem.
|
||||
static constexpr uint64_t kMinReapplyConfigFrequencySecs = 15;
|
||||
|
||||
namespace santa::santad::data_layer {
|
||||
|
||||
// Type aliases
|
||||
using ValidatorBlock = bool (^)(id, NSError **);
|
||||
using PathAndTypePair = std::pair<std::string, WatchItemPathType>;
|
||||
using PathList = std::vector<PathAndTypePair>;
|
||||
using ProcessList = std::vector<WatchItemPolicy::Process>;
|
||||
|
||||
static void PopulateError(NSError **err, NSString *msg) {
|
||||
if (err) {
|
||||
*err = [NSError errorWithDomain:@"com.google.santa.watchitems"
|
||||
code:0
|
||||
userInfo:@{NSLocalizedDescriptionKey : msg}];
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure the given string has the expected length and only
|
||||
/// contains valid hex digits
|
||||
bool ConfirmValidHexString(NSString *str, size_t expected_length) {
|
||||
if (str.length != expected_length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < str.length; i++) {
|
||||
if (!isxdigit([str characterAtIndex:i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> HexStringToBytes(NSString *str) {
|
||||
if (!str) {
|
||||
return std::vector<uint8_t>{};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> bytes;
|
||||
bytes.reserve(str.length / 2);
|
||||
|
||||
char cur_byte[3];
|
||||
cur_byte[2] = '\0';
|
||||
|
||||
for (int i = 0; i < str.length / 2; i++) {
|
||||
cur_byte[0] = [str characterAtIndex:(i * 2)];
|
||||
cur_byte[1] = [str characterAtIndex:(i * 2 + 1)];
|
||||
|
||||
bytes.push_back(std::strtoul(cur_byte, nullptr, 16));
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Given a length, returns a ValidatorBlock that confirms the
|
||||
// string is a valid hex string of the given length.
|
||||
ValidatorBlock HexValidator(NSUInteger expected_length) {
|
||||
return ^bool(NSString *val, NSError **err) {
|
||||
if (!ConfirmValidHexString(val, expected_length)) {
|
||||
PopulateError(
|
||||
err, [NSString stringWithFormat:@"Expected hex string of length %lu", expected_length]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// Given a max length, returns a ValidatorBlock that confirms the
|
||||
// string is a not longer than the max.
|
||||
ValidatorBlock LenRangeValidator(NSUInteger min_length, NSUInteger max_length) {
|
||||
return ^bool(NSString *val, NSError **err) {
|
||||
if (val.length < min_length) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Value too short. Got: %lu, Min: %lu",
|
||||
val.length, min_length]);
|
||||
return false;
|
||||
} else if (val.length > max_length) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Value too long. Got: %lu, Max: %lu",
|
||||
val.length, max_length]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/// Ensure the key exists (if required) and the value matches the expected type
|
||||
bool VerifyConfigKey(NSDictionary *dict, const NSString *key, Class expected, NSError **err,
|
||||
bool required = false, bool (^Validator)(id, NSError **) = nil) {
|
||||
if (dict[key]) {
|
||||
if (![dict[key] isKindOfClass:expected]) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Expected type '%@' for key '%@' (got: %@)",
|
||||
NSStringFromClass(expected), key,
|
||||
NSStringFromClass([dict[key] class])]);
|
||||
return false;
|
||||
}
|
||||
|
||||
NSError *validator_err;
|
||||
if (Validator && !Validator(dict[key], &validator_err)) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Invalid content in key '%@': %@", key,
|
||||
validator_err.localizedDescription]);
|
||||
return false;
|
||||
}
|
||||
} else if (required) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Missing required key '%@'", key]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Ensure all values of the array key in the dictionary conform to the
|
||||
/// expected type. If a Validator block is supplied, each item is also
|
||||
/// subject to the custom validation method.
|
||||
bool VerifyConfigKeyArray(NSDictionary *dict, NSString *key, Class expected, NSError **err,
|
||||
bool (^Validator)(id, NSError **) = nil) {
|
||||
if (!VerifyConfigKey(dict, key, [NSArray class], err)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
__block bool success = true;
|
||||
__block NSError *block_err;
|
||||
|
||||
[dict[key] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
||||
if (![obj isKindOfClass:expected]) {
|
||||
success = false;
|
||||
PopulateError(&block_err,
|
||||
[NSString stringWithFormat:@"Expected all '%@' types in array key '%@'",
|
||||
NSStringFromClass(expected), key]);
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *validator_err;
|
||||
if (Validator && !Validator(obj, &validator_err)) {
|
||||
PopulateError(&block_err,
|
||||
[NSString stringWithFormat:@"Invalid content in array key '%@': %@", key,
|
||||
validator_err.localizedDescription]);
|
||||
success = false;
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
}];
|
||||
|
||||
if (!success && block_err) {
|
||||
PopulateError(err, block_err.localizedDescription);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// The `Paths` array can contain only `string` and `dict` types:
|
||||
/// - For `string` types, the default path type `kDefaultPathType` is used
|
||||
/// - For `dict` types, there is a required `Path` key. and an optional
|
||||
/// `IsPrefix` key to set the path type to something other than the default
|
||||
///
|
||||
/// Example:
|
||||
/// <array>
|
||||
/// <string>/my/path</string>
|
||||
/// <dict>
|
||||
/// <key>Path</key>
|
||||
/// <string>/another/partial/path</string>
|
||||
/// <key>IsPrefix</key>
|
||||
/// <true/>
|
||||
/// </dict>
|
||||
/// </array>
|
||||
std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSError **err) {
|
||||
PathList path_list;
|
||||
|
||||
for (id path in paths) {
|
||||
if ([path isKindOfClass:[NSDictionary class]]) {
|
||||
NSDictionary *path_dict = (NSDictionary *)path;
|
||||
if (!VerifyConfigKey(path_dict, kWatchItemConfigKeyPathsPath, [NSString class], err, true,
|
||||
LenRangeValidator(1, PATH_MAX))) {
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
NSString *path_str = path_dict[kWatchItemConfigKeyPathsPath];
|
||||
WatchItemPathType path_type = kWatchItemPolicyDefaultPathType;
|
||||
|
||||
if (VerifyConfigKey(path_dict, kWatchItemConfigKeyPathsIsPrefix, [NSNumber class], err)) {
|
||||
path_type = ([(NSNumber *)path_dict[kWatchItemConfigKeyPathsIsPrefix] boolValue] == NO
|
||||
? WatchItemPathType::kLiteral
|
||||
: WatchItemPathType::kPrefix);
|
||||
} else {
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
path_list.push_back({std::string(path_str.UTF8String, path_str.length), path_type});
|
||||
} else if ([path isKindOfClass:[NSString class]]) {
|
||||
if (!LenRangeValidator(1, PATH_MAX)(path, err)) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Invalid path length: %@",
|
||||
(err && *err) ? (*err).localizedDescription
|
||||
: @"Unknown error"]);
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
path_list.push_back({std::string(((NSString *)path).UTF8String, ((NSString *)path).length),
|
||||
kWatchItemPolicyDefaultPathType});
|
||||
} else {
|
||||
PopulateError(
|
||||
err, [NSString stringWithFormat:
|
||||
@"%@ array item with invalid type. Expected 'dict' or 'string' (got: %@)",
|
||||
kWatchItemConfigKeyPaths, NSStringFromClass([path class])]);
|
||||
return Unit{};
|
||||
}
|
||||
}
|
||||
|
||||
if (path_list.size() == 0) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"No paths specified"]);
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
return path_list;
|
||||
}
|
||||
|
||||
/// The `Processes` array can only contain dictionaries. Each dictionary can
|
||||
/// contain the attributes that describe a single process.
|
||||
///
|
||||
/// <array>
|
||||
/// <dict>
|
||||
/// <key>BinaryPath</key>
|
||||
/// <string>AAAA</string>
|
||||
/// <key>TeamID</key>
|
||||
/// <string>BBBB</string>
|
||||
/// </dict>
|
||||
/// <dict>
|
||||
/// <key>CertificateSha256</key>
|
||||
/// <string>CCCC</string>
|
||||
/// <key>CDHash</key>
|
||||
/// <string>DDDD</string>
|
||||
/// <key>SigningID</key>
|
||||
/// <string>EEEE</string>
|
||||
/// </dict>
|
||||
/// </array>
|
||||
std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *watch_item,
|
||||
NSError **err) {
|
||||
__block ProcessList proc_list;
|
||||
|
||||
if (!VerifyConfigKeyArray(
|
||||
watch_item, kWatchItemConfigKeyProcesses, [NSDictionary class], err,
|
||||
^bool(NSDictionary *process, NSError **err) {
|
||||
if (!VerifyConfigKey(process, kWatchItemConfigKeyProcessesBinaryPath, [NSString class],
|
||||
err, false, LenRangeValidator(1, PATH_MAX)) ||
|
||||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesSigningID, [NSString class],
|
||||
err, false, LenRangeValidator(1, kMaxSigningIDLength)) ||
|
||||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesTeamID, [NSString class], err,
|
||||
false, LenRangeValidator(kMaxTeamIDLength, kMaxTeamIDLength)) ||
|
||||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesCDHash, [NSString class], err,
|
||||
false, HexValidator(CS_CDHASH_LEN * 2)) ||
|
||||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesCertificateSha256,
|
||||
[NSString class], err, false,
|
||||
HexValidator(CC_SHA256_DIGEST_LENGTH * 2))) {
|
||||
PopulateError(err, @"Failed to verify key content");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure at least one attribute set
|
||||
if (!process[kWatchItemConfigKeyProcessesBinaryPath] &&
|
||||
!process[kWatchItemConfigKeyProcessesSigningID] &&
|
||||
!process[kWatchItemConfigKeyProcessesTeamID] &&
|
||||
!process[kWatchItemConfigKeyProcessesCDHash] &&
|
||||
!process[kWatchItemConfigKeyProcessesCertificateSha256]) {
|
||||
PopulateError(err, @"No valid attributes set in process dictionary");
|
||||
return false;
|
||||
}
|
||||
|
||||
proc_list.push_back(WatchItemPolicy::Process(
|
||||
std::string([(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @"") UTF8String]),
|
||||
std::string([(process[kWatchItemConfigKeyProcessesSigningID] ?: @"") UTF8String]),
|
||||
std::string([(process[kWatchItemConfigKeyProcessesTeamID] ?: @"") UTF8String]),
|
||||
HexStringToBytes(process[kWatchItemConfigKeyProcessesCDHash]),
|
||||
std::string(
|
||||
[(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @"") UTF8String])));
|
||||
|
||||
return true;
|
||||
})) {
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
return proc_list;
|
||||
}
|
||||
|
||||
/// Ensure that a given watch item conforms to expected structure
|
||||
///
|
||||
/// Example:
|
||||
/// <dict>
|
||||
/// <key>Paths</key>
|
||||
/// <array>
|
||||
/// ... See VerifyConfigWatchItemPaths for more details ...
|
||||
/// </array>
|
||||
/// <key>Options</key>
|
||||
/// <dict>
|
||||
/// <key>AllowReadAccess</key>
|
||||
/// <false/>
|
||||
/// <key>AuditOnly</key>
|
||||
/// <false/>
|
||||
/// </dict>
|
||||
/// <key>Processes</key>
|
||||
/// <array>
|
||||
/// ... See VerifyConfigWatchItemProcesses for more details ...
|
||||
/// </array>
|
||||
/// </dict>
|
||||
bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
std::vector<std::shared_ptr<WatchItemPolicy>> &policies,
|
||||
NSError **err) {
|
||||
if (!VerifyConfigKey(watch_item, kWatchItemConfigKeyPaths, [NSArray class], err, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::variant<Unit, PathList> path_list =
|
||||
VerifyConfigWatchItemPaths(watch_item[kWatchItemConfigKeyPaths], err);
|
||||
|
||||
if (std::holds_alternative<Unit>(path_list)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(watch_item, kWatchItemConfigKeyOptions, [NSDictionary class], err)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NSDictionary *options = watch_item[kWatchItemConfigKeyOptions];
|
||||
if (options) {
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAllowReadAccess, [NSNumber class],
|
||||
err)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAuditOnly, [NSNumber class], err)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool allow_read_access = options[kWatchItemConfigKeyOptionsAllowReadAccess]
|
||||
? [options[kWatchItemConfigKeyOptionsAllowReadAccess] boolValue]
|
||||
: kWatchItemPolicyDefaultAllowReadAccess;
|
||||
bool audit_only = options[kWatchItemConfigKeyOptionsAuditOnly]
|
||||
? [options[kWatchItemConfigKeyOptionsAuditOnly] boolValue]
|
||||
: kWatchItemPolicyDefaultAuditOnly;
|
||||
|
||||
std::variant<Unit, ProcessList> proc_list = VerifyConfigWatchItemProcesses(watch_item, err);
|
||||
if (std::holds_alternative<Unit>(proc_list)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const PathAndTypePair &path_type_pair : std::get<PathList>(path_list)) {
|
||||
policies.push_back(std::make_shared<WatchItemPolicy>(
|
||||
[name UTF8String], path_type_pair.first, path_type_pair.second, allow_read_access, audit_only,
|
||||
std::get<ProcessList>(proc_list)));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPolicy>> &policies,
|
||||
NSError **err) {
|
||||
if (![config[kWatchItemConfigKeyVersion] isKindOfClass:[NSString class]]) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Missing top level string key '%@'",
|
||||
kWatchItemConfigKeyVersion]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ([(NSString *)config[kWatchItemConfigKeyVersion] length] == 0) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Top level key '%@' has empty value",
|
||||
kWatchItemConfigKeyVersion]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config[kWatchItemConfigKeyWatchItems] &&
|
||||
![config[kWatchItemConfigKeyWatchItems] isKindOfClass:[NSDictionary class]]) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Top level key '%@' must be a dictionary",
|
||||
kWatchItemConfigKeyWatchItems]);
|
||||
return false;
|
||||
}
|
||||
|
||||
NSDictionary *watch_items = config[kWatchItemConfigKeyWatchItems];
|
||||
|
||||
for (id key in watch_items) {
|
||||
if (![key isKindOfClass:[NSString class]]) {
|
||||
PopulateError(err,
|
||||
[NSString stringWithFormat:@"Invalid %@ key %@: Expected type '%@' (got: %@)",
|
||||
kWatchItemConfigKeyWatchItems, key,
|
||||
NSStringFromClass([NSString class]),
|
||||
NSStringFromClass([key class])]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ([(NSString *)key length] == 0) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Invalid %@ key with length zero",
|
||||
kWatchItemConfigKeyWatchItems]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (![watch_items[key] isKindOfClass:[NSDictionary class]]) {
|
||||
PopulateError(
|
||||
err,
|
||||
[NSString stringWithFormat:@"Value type for watch item '%@' must be a dictionary (got %@)",
|
||||
key, NSStringFromClass([watch_items[key] class])]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ParseConfigSingleWatchItem(key, watch_items[key], policies, err)) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"In watch item '%@': %@", key,
|
||||
(err && *err) ? (*err).localizedDescription
|
||||
: @"Unknown failure"]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<WatchItems> WatchItems::Create(NSString *config_path,
|
||||
uint64_t reapply_config_frequency_secs) {
|
||||
if (reapply_config_frequency_secs < kMinReapplyConfigFrequencySecs) {
|
||||
LOGW(@"Invalid watch item update interval provided: %llu. Min allowed: %llu",
|
||||
reapply_config_frequency_secs, kMinReapplyConfigFrequencySecs);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
dispatch_queue_t q = dispatch_queue_create("com.google.santa.daemon.watch_items.q",
|
||||
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
|
||||
dispatch_source_t timer_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
|
||||
dispatch_source_set_timer(timer_source, dispatch_time(DISPATCH_TIME_NOW, 0),
|
||||
NSEC_PER_SEC * reapply_config_frequency_secs, 0);
|
||||
|
||||
return std::make_shared<WatchItems>(config_path, q, timer_source);
|
||||
}
|
||||
|
||||
WatchItems::WatchItems(NSString *config_path, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
void (^periodic_task_complete_f)(void))
|
||||
: config_path_(config_path),
|
||||
q_(q),
|
||||
timer_source_(timer_source),
|
||||
periodic_task_complete_f_(periodic_task_complete_f),
|
||||
watch_items_(std::make_unique<WatchItemsTree>()) {}
|
||||
|
||||
WatchItems::~WatchItems() {
|
||||
if (!periodic_task_started_ && timer_source_ != NULL) {
|
||||
// The timer_source_ must be resumed to ensure it has a proper retain count before being
|
||||
// destroyed. Additionally, it should first be cancelled to ensure the timer isn't ever
|
||||
// fired (see man page for `dispatch_source_cancel(3)`).
|
||||
dispatch_source_cancel(timer_source_);
|
||||
dispatch_resume(timer_source_);
|
||||
}
|
||||
}
|
||||
|
||||
bool WatchItems::BuildPolicyTree(const std::vector<std::shared_ptr<WatchItemPolicy>> &watch_items,
|
||||
PrefixTree<std::shared_ptr<WatchItemPolicy>> &tree,
|
||||
std::set<std::pair<std::string, WatchItemPathType>> &paths) {
|
||||
glob_t *g = (glob_t *)alloca(sizeof(glob_t));
|
||||
for (const std::shared_ptr<WatchItemPolicy> &item : watch_items) {
|
||||
int err = glob(item->path.c_str(), 0, nullptr, g);
|
||||
if (err != 0 && err != GLOB_NOMATCH) {
|
||||
LOGE(@"Failed to generate path names for watch item: %s", item->name.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = g->gl_offs; i < g->gl_pathc; i++) {
|
||||
if (item->path_type == WatchItemPathType::kPrefix) {
|
||||
tree.InsertPrefix(g->gl_pathv[i], item);
|
||||
} else {
|
||||
tree.InsertLiteral(g->gl_pathv[i], item);
|
||||
}
|
||||
|
||||
paths.insert({g->gl_pathv[i], item->path_type});
|
||||
}
|
||||
globfree(g);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WatchItems::RegisterClient(id<SNTEndpointSecurityDynamicEventHandler> client) {
|
||||
absl::MutexLock lock(&lock_);
|
||||
registerd_clients_.insert(client);
|
||||
}
|
||||
|
||||
void WatchItems::UpdateCurrentState(
|
||||
std::unique_ptr<PrefixTree<std::shared_ptr<WatchItemPolicy>>> new_tree,
|
||||
std::set<std::pair<std::string, WatchItemPathType>> &&new_monitored_paths,
|
||||
NSDictionary *new_config) {
|
||||
absl::MutexLock lock(&lock_);
|
||||
|
||||
// The following conditions require updating the current config:
|
||||
// 1. The current config doesn't exist but the new one does
|
||||
// 2. The current config exists but the new one doesn't
|
||||
// 3. The set of monitored paths changed
|
||||
// 4. The configuration changed
|
||||
if ((current_config_ != nil && new_config == nil) ||
|
||||
(current_config_ == nil && new_config != nil) ||
|
||||
(currently_monitored_paths_ != new_monitored_paths) ||
|
||||
(new_config && ![current_config_ isEqualToDictionary:new_config])) {
|
||||
std::vector<std::pair<std::string, WatchItemPathType>> paths_to_watch;
|
||||
std::vector<std::pair<std::string, WatchItemPathType>> paths_to_stop_watching;
|
||||
|
||||
// New paths to watch are those that are in the new set, but not current
|
||||
std::set_difference(new_monitored_paths.begin(), new_monitored_paths.end(),
|
||||
currently_monitored_paths_.begin(), currently_monitored_paths_.end(),
|
||||
std::back_inserter(paths_to_watch));
|
||||
|
||||
// Paths to stop watching are in the current set, but not new
|
||||
std::set_difference(currently_monitored_paths_.begin(), currently_monitored_paths_.end(),
|
||||
new_monitored_paths.begin(), new_monitored_paths.end(),
|
||||
std::back_inserter(paths_to_stop_watching));
|
||||
|
||||
std::swap(watch_items_, new_tree);
|
||||
std::swap(currently_monitored_paths_, new_monitored_paths);
|
||||
current_config_ = new_config;
|
||||
if (new_config) {
|
||||
policy_version_ = [new_config[kWatchItemConfigKeyVersion] UTF8String];
|
||||
} else {
|
||||
policy_version_ = "";
|
||||
}
|
||||
|
||||
last_update_time_ = [[NSDate date] timeIntervalSince1970];
|
||||
|
||||
for (const id<SNTEndpointSecurityDynamicEventHandler> &client : registerd_clients_) {
|
||||
// Note: Enable clients on an async queue in case they perform any
|
||||
// synchronous work that could trigger ES events. Otherwise they might
|
||||
// trigger AUTH ES events that would attempt to re-enter this object and
|
||||
// potentially deadlock.
|
||||
dispatch_async(q_, ^{
|
||||
[client watchItemsCount:currently_monitored_paths_.size()
|
||||
newPaths:paths_to_watch
|
||||
removedPaths:paths_to_stop_watching];
|
||||
});
|
||||
}
|
||||
} else {
|
||||
LOGD(@"No changes to set of watched paths.");
|
||||
}
|
||||
}
|
||||
|
||||
void WatchItems::ReloadConfig(NSDictionary *new_config) {
|
||||
std::vector<std::shared_ptr<WatchItemPolicy>> new_policies;
|
||||
auto new_tree = std::make_unique<PrefixTree<std::shared_ptr<WatchItemPolicy>>>();
|
||||
std::set<std::pair<std::string, WatchItemPathType>> new_monitored_paths;
|
||||
|
||||
if (new_config) {
|
||||
NSError *err;
|
||||
if (!ParseConfig(new_config, new_policies, &err)) {
|
||||
LOGE(@"Failed to parse watch item config: %@",
|
||||
err ? err.localizedDescription : @"Unknown failure");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!BuildPolicyTree(new_policies, *new_tree, new_monitored_paths)) {
|
||||
LOGE(@"Failed to build new filesystem monitoring policy");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateCurrentState(std::move(new_tree), std::move(new_monitored_paths), new_config);
|
||||
}
|
||||
|
||||
NSDictionary *WatchItems::ReadConfig() {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
return ReadConfigLocked();
|
||||
}
|
||||
|
||||
NSDictionary *WatchItems::ReadConfigLocked() {
|
||||
if (config_path_) {
|
||||
return [NSDictionary dictionaryWithContentsOfFile:config_path_];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
void WatchItems::BeginPeriodicTask() {
|
||||
if (periodic_task_started_) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::weak_ptr<WatchItems> weak_watcher = weak_from_this();
|
||||
dispatch_source_set_event_handler(timer_source_, ^{
|
||||
std::shared_ptr<WatchItems> shared_watcher = weak_watcher.lock();
|
||||
if (!shared_watcher) {
|
||||
return;
|
||||
}
|
||||
|
||||
shared_watcher->ReloadConfig(shared_watcher->ReadConfig());
|
||||
|
||||
if (shared_watcher->periodic_task_complete_f_) {
|
||||
shared_watcher->periodic_task_complete_f_();
|
||||
}
|
||||
});
|
||||
|
||||
dispatch_resume(timer_source_);
|
||||
periodic_task_started_ = true;
|
||||
}
|
||||
|
||||
WatchItems::VersionAndPolicies WatchItems::FindPolciesForPaths(
|
||||
const std::vector<std::string_view> &paths) {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
std::vector<std::optional<std::shared_ptr<WatchItemPolicy>>> policies;
|
||||
|
||||
for (const auto &path : paths) {
|
||||
policies.push_back(watch_items_->LookupLongestMatchingPrefix(path.data()));
|
||||
}
|
||||
|
||||
return {policy_version_, policies};
|
||||
}
|
||||
|
||||
void WatchItems::SetConfigPath(NSString *config_path) {
|
||||
// Acquire the lock to set the config path and read the config, but drop
|
||||
// the lock before reloading the config
|
||||
NSDictionary *config;
|
||||
{
|
||||
absl::MutexLock lock(&lock_);
|
||||
config_path_ = config_path;
|
||||
config = ReadConfigLocked();
|
||||
}
|
||||
ReloadConfig(config);
|
||||
}
|
||||
|
||||
std::optional<WatchItemsState> WatchItems::State() {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
|
||||
if (!current_config_) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
WatchItemsState state = {
|
||||
.rule_count = [current_config_[kWatchItemConfigKeyWatchItems] count],
|
||||
.policy_version = [NSString stringWithUTF8String:policy_version_.c_str()],
|
||||
.config_path = [config_path_ copy],
|
||||
.last_config_load_epoch = last_update_time_,
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
807
Source/santad/DataLayer/WatchItemsTest.mm
Normal file
807
Source/santad/DataLayer/WatchItemsTest.mm
Normal file
@@ -0,0 +1,807 @@
|
||||
/// Copyright 2022 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 <CommonCrypto/CommonDigest.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <sys/syslimits.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#import "Source/common/Unit.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/DataLayer/WatchItems.h"
|
||||
|
||||
using santa::common::Unit;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultAllowReadAccess;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultAuditOnly;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultPathType;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
using santa::santad::data_layer::WatchItems;
|
||||
using santa::santad::data_layer::WatchItemsState;
|
||||
|
||||
namespace santatest {
|
||||
using PathAndTypePair = std::pair<std::string, WatchItemPathType>;
|
||||
using PathList = std::vector<PathAndTypePair>;
|
||||
using ProcessList = std::vector<WatchItemPolicy::Process>;
|
||||
} // namespace santatest
|
||||
|
||||
namespace santa::santad::data_layer {
|
||||
|
||||
extern bool ParseConfig(NSDictionary *config,
|
||||
std::vector<std::shared_ptr<WatchItemPolicy>> &policies, NSError **err);
|
||||
extern bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
std::vector<std::shared_ptr<WatchItemPolicy>> &policies,
|
||||
NSError **err);
|
||||
extern std::variant<Unit, santatest::PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths,
|
||||
NSError **err);
|
||||
extern std::variant<Unit, santatest::ProcessList> VerifyConfigWatchItemProcesses(
|
||||
NSDictionary *watch_item, NSError **err);
|
||||
class WatchItemsPeer : public WatchItems {
|
||||
public:
|
||||
using WatchItems::ReloadConfig;
|
||||
using WatchItems::WatchItems;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
|
||||
using santa::santad::data_layer::ParseConfig;
|
||||
using santa::santad::data_layer::ParseConfigSingleWatchItem;
|
||||
using santa::santad::data_layer::VerifyConfigWatchItemPaths;
|
||||
using santa::santad::data_layer::VerifyConfigWatchItemProcesses;
|
||||
using santa::santad::data_layer::WatchItemsPeer;
|
||||
|
||||
static constexpr std::string_view kBadPolicyName("__BAD_NAME__");
|
||||
static constexpr std::string_view kBadPolicyPath("__BAD_PATH__");
|
||||
static constexpr std::string_view kVersion("v0.1");
|
||||
|
||||
static std::shared_ptr<WatchItemPolicy> MakeBadPolicy() {
|
||||
return std::make_shared<WatchItemPolicy>(kBadPolicyName, kBadPolicyPath);
|
||||
}
|
||||
|
||||
static NSMutableDictionary *WrapWatchItemsConfig(NSDictionary *config) {
|
||||
return [@{@"Version" : @(kVersion.data()), @"WatchItems" : [config mutableCopy]} mutableCopy];
|
||||
}
|
||||
|
||||
static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
return [@"" stringByPaddingToLength:len withString:str startingAtIndex:0];
|
||||
}
|
||||
|
||||
@interface WatchItemsTest : XCTestCase
|
||||
@property NSFileManager *fileMgr;
|
||||
@property NSString *testDir;
|
||||
@property NSMutableArray *dirStack;
|
||||
@property dispatch_queue_t q;
|
||||
@end
|
||||
|
||||
@implementation WatchItemsTest
|
||||
|
||||
- (void)setUp {
|
||||
self.dirStack = [[NSMutableArray alloc] init];
|
||||
self.fileMgr = [NSFileManager defaultManager];
|
||||
self.testDir =
|
||||
[NSString stringWithFormat:@"%@santa-watchitems-%d", NSTemporaryDirectory(), getpid()];
|
||||
|
||||
XCTAssertTrue([self.fileMgr createDirectoryAtPath:self.testDir
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:nil]);
|
||||
|
||||
self.q = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
|
||||
XCTAssertNotNil(self.q);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
XCTAssertTrue([self.fileMgr removeItemAtPath:self.testDir error:nil]);
|
||||
}
|
||||
|
||||
- (void)pushd:(NSString *)path withRoot:(NSString *)root {
|
||||
NSString *dir = [NSString pathWithComponents:@[ root, path ]];
|
||||
NSString *origCwd = [self.fileMgr currentDirectoryPath];
|
||||
XCTAssertNotNil(origCwd);
|
||||
|
||||
XCTAssertTrue([self.fileMgr changeCurrentDirectoryPath:dir]);
|
||||
[self.dirStack addObject:origCwd];
|
||||
}
|
||||
|
||||
- (void)pushd:(NSString *)dir {
|
||||
[self pushd:dir withRoot:self.testDir];
|
||||
}
|
||||
|
||||
- (void)popd {
|
||||
NSString *dir = [self.dirStack lastObject];
|
||||
XCTAssertTrue([self.fileMgr changeCurrentDirectoryPath:dir]);
|
||||
[self.dirStack removeLastObject];
|
||||
}
|
||||
|
||||
- (void)createTestDirStructure:(NSArray *)fs rootedAt:(NSString *)root {
|
||||
NSString *origCwd = [self.fileMgr currentDirectoryPath];
|
||||
XCTAssertNotNil(origCwd);
|
||||
XCTAssertTrue([self.fileMgr changeCurrentDirectoryPath:root]);
|
||||
|
||||
for (id item in fs) {
|
||||
if ([item isKindOfClass:[NSString class]]) {
|
||||
XCTAssertTrue([self.fileMgr createFileAtPath:item contents:nil attributes:nil]);
|
||||
} else if ([item isKindOfClass:[NSDictionary class]]) {
|
||||
for (id dir in item) {
|
||||
XCTAssertTrue([item[dir] isKindOfClass:[NSArray class]]);
|
||||
XCTAssertTrue([self.fileMgr createDirectoryAtPath:dir
|
||||
withIntermediateDirectories:NO
|
||||
attributes:nil
|
||||
error:nil]);
|
||||
|
||||
[self createTestDirStructure:item[dir] rootedAt:dir];
|
||||
}
|
||||
} else {
|
||||
XCTFail("Unexpected dir structure item: %@: %@", item, [item class]);
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertTrue([self.fileMgr changeCurrentDirectoryPath:origCwd]);
|
||||
}
|
||||
|
||||
- (void)createTestDirStructure:(NSArray *)fs {
|
||||
[self createTestDirStructure:fs rootedAt:self.testDir];
|
||||
}
|
||||
|
||||
- (void)testReloadScenarios {
|
||||
[self createTestDirStructure:@[
|
||||
@{
|
||||
@"a" : @[ @"f1", @"f2" ],
|
||||
},
|
||||
@{
|
||||
@"b" : @[ @"f1" ],
|
||||
},
|
||||
]];
|
||||
|
||||
NSDictionary *allFilesPolicy = @{kWatchItemConfigKeyPaths : @[ @"*" ]};
|
||||
NSDictionary *configAllFilesOriginal =
|
||||
WrapWatchItemsConfig(@{@"all_files_orig" : allFilesPolicy});
|
||||
NSDictionary *configAllFilesRename =
|
||||
WrapWatchItemsConfig(@{@"all_files_rename" : allFilesPolicy});
|
||||
|
||||
WatchItems::VersionAndPolicies policies;
|
||||
|
||||
std::vector<std::string_view> f1Path = {"f1"};
|
||||
std::vector<std::string_view> f2Path = {"f2"};
|
||||
|
||||
// Changes in config dictionary will update policy info even if the
|
||||
// filesystem didn't change.
|
||||
{
|
||||
WatchItemsPeer watchItems(nil, NULL, NULL);
|
||||
[self pushd:@"a"];
|
||||
watchItems.ReloadConfig(configAllFilesOriginal);
|
||||
|
||||
policies = watchItems.FindPolciesForPaths(f1Path);
|
||||
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
|
||||
"all_files_orig");
|
||||
|
||||
watchItems.ReloadConfig(configAllFilesRename);
|
||||
policies = watchItems.FindPolciesForPaths(f1Path);
|
||||
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
|
||||
"all_files_rename");
|
||||
|
||||
policies = watchItems.FindPolciesForPaths(f1Path);
|
||||
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
|
||||
"all_files_rename");
|
||||
[self popd];
|
||||
}
|
||||
|
||||
// Changes to fileystem structure are reflected when a config is reloaded
|
||||
{
|
||||
WatchItemsPeer watchItems(nil, NULL, NULL);
|
||||
[self pushd:@"a"];
|
||||
watchItems.ReloadConfig(configAllFilesOriginal);
|
||||
[self popd];
|
||||
|
||||
policies = watchItems.FindPolciesForPaths(f2Path);
|
||||
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
|
||||
"all_files_orig");
|
||||
|
||||
[self pushd:@"b"];
|
||||
watchItems.ReloadConfig(configAllFilesOriginal);
|
||||
[self popd];
|
||||
|
||||
policies = watchItems.FindPolciesForPaths(f2Path);
|
||||
XCTAssertFalse(policies.second[0].has_value());
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testPeriodicTask {
|
||||
// Ensure watch item policy memory is properly handled
|
||||
[self createTestDirStructure:@[ @"f1", @"f2", @"weird1" ]];
|
||||
|
||||
NSDictionary *fFiles = @{
|
||||
kWatchItemConfigKeyPaths : @[ @{
|
||||
kWatchItemConfigKeyPathsPath : @"f?",
|
||||
kWatchItemConfigKeyPathsIsPrefix : @(NO),
|
||||
} ]
|
||||
};
|
||||
NSDictionary *weirdFiles = @{
|
||||
kWatchItemConfigKeyPaths : @[ @{
|
||||
kWatchItemConfigKeyPathsPath : @"weird?",
|
||||
kWatchItemConfigKeyPathsIsPrefix : @(NO),
|
||||
} ]
|
||||
};
|
||||
|
||||
NSString *configFile = @"config.plist";
|
||||
NSDictionary *firstConfig = WrapWatchItemsConfig(@{@"f_files" : fFiles});
|
||||
NSDictionary *secondConfig =
|
||||
WrapWatchItemsConfig(@{@"f_files" : fFiles, @"weird_files" : weirdFiles});
|
||||
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
|
||||
const uint64 periodicFlushMS = 1000;
|
||||
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0),
|
||||
NSEC_PER_MSEC * periodicFlushMS, 0);
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
auto watchItems = std::make_shared<WatchItemsPeer>(configFile, self.q, timer, ^{
|
||||
dispatch_semaphore_signal(sema);
|
||||
});
|
||||
|
||||
// Move into the base test directory and write the config to disk
|
||||
[self pushd:@""];
|
||||
XCTAssertTrue([firstConfig writeToFile:configFile atomically:YES]);
|
||||
|
||||
std::vector<std::string_view> f1Path = {"f1"};
|
||||
std::vector<std::string_view> weird1Path = {"weird1"};
|
||||
|
||||
// Ensure no policy has been loaded yet
|
||||
XCTAssertFalse(watchItems->FindPolciesForPaths(f1Path).second[0].has_value());
|
||||
XCTAssertFalse(watchItems->FindPolciesForPaths(weird1Path).second[0].has_value());
|
||||
|
||||
// Begin the periodic task
|
||||
watchItems->BeginPeriodicTask();
|
||||
|
||||
// The first run of the task starts immediately
|
||||
// Wait for the first iteration and check for the expected policy
|
||||
XCTAssertSemaTrue(sema, 5, "Periodic task did not complete within expected window");
|
||||
XCTAssertTrue(watchItems->FindPolciesForPaths(f1Path).second[0].has_value());
|
||||
XCTAssertFalse(watchItems->FindPolciesForPaths(weird1Path).second[0].has_value());
|
||||
|
||||
// Write the config update
|
||||
XCTAssertTrue([secondConfig writeToFile:configFile atomically:YES]);
|
||||
|
||||
// Wait for the new config to be loaded and check for the new expected policies
|
||||
XCTAssertSemaTrue(sema, 5, "Periodic task did not complete within expected window");
|
||||
XCTAssertTrue(watchItems->FindPolciesForPaths(f1Path).second[0].has_value());
|
||||
XCTAssertTrue(watchItems->FindPolciesForPaths(weird1Path).second[0].has_value());
|
||||
|
||||
[self popd];
|
||||
}
|
||||
|
||||
- (void)testPolicyLookup {
|
||||
// Test multiple, more comprehensive policies before/after config reload
|
||||
[self createTestDirStructure:@[
|
||||
@{
|
||||
@"foo" : @[ @"bar.txt", @"bar.txt.tmp" ],
|
||||
@"baz" : @[ @{@"qaz" : @[]} ],
|
||||
},
|
||||
@"f1",
|
||||
]];
|
||||
|
||||
NSMutableDictionary *config = WrapWatchItemsConfig(@{
|
||||
@"foo_subdir" : @{
|
||||
kWatchItemConfigKeyPaths : @[ @{
|
||||
kWatchItemConfigKeyPathsPath : @"./foo",
|
||||
kWatchItemConfigKeyPathsIsPrefix : @(YES),
|
||||
} ]
|
||||
}
|
||||
});
|
||||
|
||||
WatchItemsPeer watchItems(nil, NULL, NULL);
|
||||
WatchItems::VersionAndPolicies policies;
|
||||
|
||||
// Resultant vector is same size as input vector
|
||||
// Initially nothing should be in the map
|
||||
std::vector<std::string_view> paths = {};
|
||||
XCTAssertEqual(watchItems.FindPolciesForPaths(paths).second.size(), 0);
|
||||
paths.push_back("./foo");
|
||||
XCTAssertEqual(watchItems.FindPolciesForPaths(paths).second.size(), 1);
|
||||
XCTAssertFalse(watchItems.FindPolciesForPaths(paths).second[0].has_value());
|
||||
paths.push_back("./baz");
|
||||
XCTAssertEqual(watchItems.FindPolciesForPaths(paths).second.size(), 2);
|
||||
|
||||
// Load the initial config
|
||||
[self pushd:@""];
|
||||
watchItems.ReloadConfig(config);
|
||||
[self popd];
|
||||
|
||||
{
|
||||
// Test expected values with the inital policy
|
||||
const std::map<std::vector<std::string_view>, std::string_view> pathToPolicyName = {
|
||||
{{"./foo"}, "foo_subdir"},
|
||||
{{"./foo/bar.txt.tmp"}, "foo_subdir"},
|
||||
{{"./foo/bar.txt"}, "foo_subdir"},
|
||||
{{"./does/not/exist"}, kBadPolicyName},
|
||||
};
|
||||
|
||||
for (const auto &kv : pathToPolicyName) {
|
||||
policies = watchItems.FindPolciesForPaths(kv.first);
|
||||
XCTAssertCStringEqual(policies.first.data(), kVersion.data());
|
||||
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
|
||||
kv.second.data());
|
||||
}
|
||||
|
||||
// Test multiple lookup
|
||||
policies = watchItems.FindPolciesForPaths({"./foo", "./does/not/exist"});
|
||||
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(), "foo_subdir");
|
||||
XCTAssertFalse(policies.second[1].has_value());
|
||||
}
|
||||
|
||||
// Add a new policy and reload the config
|
||||
NSDictionary *barTxtFilePolicy = @{
|
||||
kWatchItemConfigKeyPaths : @[ @{
|
||||
kWatchItemConfigKeyPathsPath : @"./foo/bar.txt",
|
||||
kWatchItemConfigKeyPathsIsPrefix : @(NO),
|
||||
} ]
|
||||
};
|
||||
[config[@"WatchItems"] setObject:barTxtFilePolicy forKey:@"bar_txt"];
|
||||
|
||||
// Load the updated config
|
||||
[self pushd:@""];
|
||||
watchItems.ReloadConfig(config);
|
||||
[self popd];
|
||||
|
||||
{
|
||||
// Test expected values with the updated policy
|
||||
const std::map<std::vector<std::string_view>, std::string_view> pathToPolicyName = {
|
||||
{{"./foo"}, "foo_subdir"},
|
||||
{{"./foo/bar.txt.tmp"}, "foo_subdir"},
|
||||
{{"./foo/bar.txt"}, "bar_txt"},
|
||||
{{"./does/not/exist"}, kBadPolicyName},
|
||||
};
|
||||
|
||||
for (const auto &kv : pathToPolicyName) {
|
||||
policies = watchItems.FindPolciesForPaths(kv.first);
|
||||
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
|
||||
kv.second.data());
|
||||
}
|
||||
}
|
||||
|
||||
// Add a catch-all policy that should only affect the previously non-matching path
|
||||
NSDictionary *catchAllFilePolicy = @{
|
||||
kWatchItemConfigKeyPaths : @[ @{
|
||||
kWatchItemConfigKeyPathsPath : @".",
|
||||
kWatchItemConfigKeyPathsIsPrefix : @(YES),
|
||||
} ]
|
||||
};
|
||||
[config[@"WatchItems"] setObject:catchAllFilePolicy forKey:@"dot_everything"];
|
||||
|
||||
// Load the updated config
|
||||
[self pushd:@""];
|
||||
watchItems.ReloadConfig(config);
|
||||
[self popd];
|
||||
|
||||
{
|
||||
// Test expected values with the catch-all policy
|
||||
const std::map<std::vector<std::string_view>, std::string_view> pathToPolicyName = {
|
||||
{{"./foo"}, "foo_subdir"},
|
||||
{{"./foo/bar.txt.tmp"}, "foo_subdir"},
|
||||
{{"./foo/bar.txt"}, "bar_txt"},
|
||||
{{"./does/not/exist"}, "dot_everything"},
|
||||
};
|
||||
|
||||
for (const auto &kv : pathToPolicyName) {
|
||||
policies = watchItems.FindPolciesForPaths(kv.first);
|
||||
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
|
||||
kv.second.data());
|
||||
}
|
||||
}
|
||||
|
||||
// Now remove the foo_subdir rule, previous matches should fallback to the catch-all
|
||||
[config[@"WatchItems"] removeObjectForKey:@"foo_subdir"];
|
||||
[self pushd:@""];
|
||||
watchItems.ReloadConfig(config);
|
||||
[self popd];
|
||||
|
||||
{
|
||||
// Test expected values with the foo_subdir policy removed
|
||||
const std::map<std::vector<std::string_view>, std::string_view> pathToPolicyName = {
|
||||
{{"./foo"}, "dot_everything"},
|
||||
{{"./foo/bar.txt.tmp"}, "dot_everything"},
|
||||
{{"./foo/bar.txt"}, "bar_txt"},
|
||||
{{"./does/not/exist"}, "dot_everything"},
|
||||
};
|
||||
|
||||
for (const auto &kv : pathToPolicyName) {
|
||||
policies = watchItems.FindPolciesForPaths(kv.first);
|
||||
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
|
||||
kv.second.data());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testVerifyConfigWatchItemPaths {
|
||||
std::variant<Unit, santatest::PathList> path_list;
|
||||
NSError *err;
|
||||
|
||||
// Test no paths specified
|
||||
path_list = VerifyConfigWatchItemPaths(@[], &err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(path_list));
|
||||
|
||||
// Test invalid types in paths array
|
||||
path_list = VerifyConfigWatchItemPaths(@[ @(0) ], &err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(path_list));
|
||||
|
||||
// Test path array with long string
|
||||
path_list = VerifyConfigWatchItemPaths(@[ RepeatedString(@"A", PATH_MAX + 1) ], &err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(path_list));
|
||||
|
||||
// Test path array dictionary with missing required key
|
||||
path_list = VerifyConfigWatchItemPaths(@[ @{@"FakePath" : @"A"} ], &err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(path_list));
|
||||
|
||||
// Test path array dictionary with long string
|
||||
path_list = VerifyConfigWatchItemPaths(
|
||||
@[ @{kWatchItemConfigKeyPathsPath : RepeatedString(@"A", PATH_MAX + 1)} ], &err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(path_list));
|
||||
|
||||
// Test path array dictionary with default path type
|
||||
path_list = VerifyConfigWatchItemPaths(@[ @{kWatchItemConfigKeyPathsPath : @"A"} ], &err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::PathList>(path_list));
|
||||
XCTAssertEqual(std::get<santatest::PathList>(path_list).size(), 1);
|
||||
XCTAssertCStringEqual(std::get<santatest::PathList>(path_list)[0].first.c_str(), "A");
|
||||
XCTAssertEqual(std::get<santatest::PathList>(path_list)[0].second,
|
||||
kWatchItemPolicyDefaultPathType);
|
||||
|
||||
// Test path array dictionary with custom path type
|
||||
path_list = VerifyConfigWatchItemPaths(
|
||||
@[ @{kWatchItemConfigKeyPathsPath : @"A", kWatchItemConfigKeyPathsIsPrefix : @(YES)} ], &err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::PathList>(path_list));
|
||||
XCTAssertEqual(std::get<santatest::PathList>(path_list).size(), 1);
|
||||
XCTAssertCStringEqual(std::get<santatest::PathList>(path_list)[0].first.c_str(), "A");
|
||||
XCTAssertEqual(std::get<santatest::PathList>(path_list)[0].second, WatchItemPathType::kPrefix);
|
||||
}
|
||||
|
||||
- (void)testVerifyConfigWatchItemProcesses {
|
||||
std::variant<Unit, santatest::ProcessList> proc_list;
|
||||
NSError *err;
|
||||
|
||||
// Non-existent process list parses successfully, but has no items
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{}, &err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 0);
|
||||
|
||||
// Process list fails to parse if contains non-array type
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{kWatchItemConfigKeyProcesses : @""}, &err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{kWatchItemConfigKeyProcesses : @(0)}, &err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{kWatchItemConfigKeyProcesses : @{}}, &err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{kWatchItemConfigKeyProcesses : @[]}, &err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
|
||||
// Test a process dictionary with no valid attributes set
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{kWatchItemConfigKeyProcesses : @[ @{} ]}, &err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
|
||||
// Test BinaryPath length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
kWatchItemConfigKeyProcesses :
|
||||
@[ @{kWatchItemConfigKeyProcessesBinaryPath : RepeatedString(@"A", PATH_MAX + 1)} ]
|
||||
},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
|
||||
// Test valid BinaryPath
|
||||
proc_list = VerifyConfigWatchItemProcesses(
|
||||
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesBinaryPath : @"mypath"} ]},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("mypath", "", "", {}, ""));
|
||||
|
||||
// Test SigningID length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
kWatchItemConfigKeyProcesses :
|
||||
@[ @{kWatchItemConfigKeyProcessesSigningID : RepeatedString(@"A", 513)} ]
|
||||
},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
|
||||
// Test valid SigningID
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
kWatchItemConfigKeyProcesses :
|
||||
@[ @{kWatchItemConfigKeyProcessesSigningID : @"com.google.test"} ]
|
||||
},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "com.google.test", "", {}, ""));
|
||||
|
||||
// Test TeamID length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
kWatchItemConfigKeyProcesses :
|
||||
@[ @{kWatchItemConfigKeyProcessesTeamID : @"LongerThanExpectedTeamID"} ]
|
||||
},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
|
||||
// Test valid TeamID
|
||||
proc_list = VerifyConfigWatchItemProcesses(
|
||||
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesTeamID : @"myvalidtid"} ]},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "", "myvalidtid", {}, ""));
|
||||
|
||||
// Test CDHash length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
kWatchItemConfigKeyProcesses :
|
||||
@[ @{kWatchItemConfigKeyProcessesCDHash : RepeatedString(@"A", CS_CDHASH_LEN * 2 + 1)} ]
|
||||
},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
|
||||
// Test CDHash hex-only
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
kWatchItemConfigKeyProcesses :
|
||||
@[ @{kWatchItemConfigKeyProcessesCDHash : RepeatedString(@"Z", CS_CDHASH_LEN * 2)} ]
|
||||
},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
|
||||
// Test valid CDHash
|
||||
NSString *cdhash = RepeatedString(@"A", CS_CDHASH_LEN * 2);
|
||||
std::vector<uint8_t> cdhashBytes(cdhash.length / 2);
|
||||
std::fill(cdhashBytes.begin(), cdhashBytes.end(), 0xAA);
|
||||
proc_list = VerifyConfigWatchItemProcesses(
|
||||
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesCDHash : cdhash} ]}, &err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "", "", cdhashBytes, ""));
|
||||
|
||||
// Test Cert Hash length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
kWatchItemConfigKeyProcesses : @[ @{
|
||||
kWatchItemConfigKeyProcessesCertificateSha256 :
|
||||
RepeatedString(@"A", CC_SHA256_DIGEST_LENGTH * 2 + 1)
|
||||
} ]
|
||||
},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
|
||||
// Test Cert Hash hex-only
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
kWatchItemConfigKeyProcesses : @[ @{
|
||||
kWatchItemConfigKeyProcessesCertificateSha256 :
|
||||
RepeatedString(@"Z", CC_SHA256_DIGEST_LENGTH * 2)
|
||||
} ]
|
||||
},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
|
||||
// Test valid Cert Hash
|
||||
NSString *certHash = RepeatedString(@"A", CC_SHA256_DIGEST_LENGTH * 2);
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesCertificateSha256 : certHash} ]
|
||||
},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "", "", {}, [certHash UTF8String]));
|
||||
|
||||
// Test valid multiple attributes, multiple procs
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
kWatchItemConfigKeyProcesses : @[
|
||||
@{
|
||||
kWatchItemConfigKeyProcessesBinaryPath : @"mypath1",
|
||||
kWatchItemConfigKeyProcessesSigningID : @"com.google.test1",
|
||||
kWatchItemConfigKeyProcessesTeamID : @"validtid_1",
|
||||
kWatchItemConfigKeyProcessesCDHash : cdhash,
|
||||
kWatchItemConfigKeyProcessesCertificateSha256 : certHash,
|
||||
},
|
||||
@{
|
||||
kWatchItemConfigKeyProcessesBinaryPath : @"mypath2",
|
||||
kWatchItemConfigKeyProcessesSigningID : @"com.google.test2",
|
||||
kWatchItemConfigKeyProcessesTeamID : @"validtid_2",
|
||||
kWatchItemConfigKeyProcessesCDHash : cdhash,
|
||||
kWatchItemConfigKeyProcessesCertificateSha256 : certHash,
|
||||
},
|
||||
]
|
||||
},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 2);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("mypath1", "com.google.test1", "validtid_1", cdhashBytes,
|
||||
[certHash UTF8String]));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[1],
|
||||
WatchItemPolicy::Process("mypath2", "com.google.test2", "validtid_2", cdhashBytes,
|
||||
[certHash UTF8String]));
|
||||
}
|
||||
|
||||
- (void)testParseConfig {
|
||||
NSError *err;
|
||||
std::vector<std::shared_ptr<WatchItemPolicy>> policies;
|
||||
|
||||
// Ensure top level keys must exist and be correct types
|
||||
XCTAssertFalse(ParseConfig(@{}, policies, &err));
|
||||
XCTAssertFalse(ParseConfig(@{kWatchItemConfigKeyVersion : @(0)}, policies, &err));
|
||||
XCTAssertFalse(ParseConfig(@{kWatchItemConfigKeyVersion : @{}}, policies, &err));
|
||||
XCTAssertFalse(ParseConfig(@{kWatchItemConfigKeyVersion : @[]}, policies, &err));
|
||||
XCTAssertFalse(ParseConfig(@{kWatchItemConfigKeyVersion : @""}, policies, &err));
|
||||
XCTAssertFalse(ParseConfig(
|
||||
@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @""}, policies, &err));
|
||||
XCTAssertFalse(ParseConfig(
|
||||
@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @[]}, policies, &err));
|
||||
XCTAssertFalse(ParseConfig(
|
||||
@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @(0)}, policies, &err));
|
||||
|
||||
// Minimally successful configs without watch items
|
||||
XCTAssertTrue(ParseConfig(@{kWatchItemConfigKeyVersion : @"1"}, policies, &err));
|
||||
XCTAssertTrue(ParseConfig(
|
||||
@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{}}, policies, &err));
|
||||
|
||||
// Ensure constraints on watch items entries match expectations
|
||||
XCTAssertFalse(ParseConfig(
|
||||
@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@(0) : @(0)}}, policies,
|
||||
&err));
|
||||
XCTAssertFalse(
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"" : @{}}},
|
||||
policies, &err));
|
||||
XCTAssertFalse(
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"1" : @[]}},
|
||||
policies, &err));
|
||||
XCTAssertFalse(
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"1" : @{}}},
|
||||
policies, &err));
|
||||
|
||||
// Minimally successful config with watch item
|
||||
XCTAssertTrue(ParseConfig(@{
|
||||
kWatchItemConfigKeyVersion : @"1",
|
||||
kWatchItemConfigKeyWatchItems : @{@"1" : @{kWatchItemConfigKeyPaths : @[ @"asdf" ]}}
|
||||
},
|
||||
policies, &err));
|
||||
}
|
||||
|
||||
- (void)testParseConfigSingleWatchItem {
|
||||
std::vector<std::shared_ptr<WatchItemPolicy>> policies;
|
||||
NSError *err;
|
||||
|
||||
// There must be valid Paths in a watch item
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{}, policies, &err));
|
||||
XCTAssertFalse(
|
||||
ParseConfigSingleWatchItem(@"", @{kWatchItemConfigKeyPaths : @[ @"" ]}, policies, &err));
|
||||
XCTAssertTrue(
|
||||
ParseConfigSingleWatchItem(@"", @{kWatchItemConfigKeyPaths : @[ @"a" ]}, policies, &err));
|
||||
|
||||
// Empty options are fine
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(
|
||||
@"", @{kWatchItemConfigKeyPaths : @[ @"a" ], kWatchItemConfigKeyOptions : @{}}, policies,
|
||||
&err));
|
||||
|
||||
// If an Options key exist, it must be a dictionary type
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(
|
||||
@"", @{kWatchItemConfigKeyPaths : @[ @"a" ], kWatchItemConfigKeyOptions : @[]}, policies,
|
||||
&err));
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(
|
||||
@"", @{kWatchItemConfigKeyPaths : @[ @"a" ], kWatchItemConfigKeyOptions : @""}, policies,
|
||||
&err));
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(
|
||||
@"", @{kWatchItemConfigKeyPaths : @[ @"a" ], kWatchItemConfigKeyOptions : @(0)}, policies,
|
||||
&err));
|
||||
|
||||
// Options keys must be valid types
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAllowReadAccess : @""}
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAllowReadAccess : @(0)}
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAuditOnly : @""}
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAuditOnly : @(0)}
|
||||
},
|
||||
policies, &err));
|
||||
|
||||
// If processes are specified, they must be valid format
|
||||
// Note: Full tests in `testVerifyConfigWatchItemProcesses`
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(
|
||||
@"", @{kWatchItemConfigKeyPaths : @[ @"a" ], kWatchItemConfigKeyProcesses : @""}, policies,
|
||||
&err));
|
||||
|
||||
// Test the policy vector is populated as expected
|
||||
|
||||
// Test default options with no processes
|
||||
policies.clear();
|
||||
XCTAssertTrue(
|
||||
ParseConfigSingleWatchItem(@"rule", @{kWatchItemConfigKeyPaths : @[ @"a" ]}, policies, &err));
|
||||
XCTAssertEqual(policies.size(), 1);
|
||||
XCTAssertEqual(*policies[0].get(), WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
|
||||
kWatchItemPolicyDefaultAllowReadAccess,
|
||||
kWatchItemPolicyDefaultAuditOnly, {}));
|
||||
|
||||
// Test multiple paths, options, and processes
|
||||
policies.clear();
|
||||
std::vector<WatchItemPolicy::Process> procs = {
|
||||
WatchItemPolicy::Process("pa", "", "", {}, ""),
|
||||
WatchItemPolicy::Process("pb", "", "", {}, ""),
|
||||
};
|
||||
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(@"rule", @{
|
||||
kWatchItemConfigKeyPaths :
|
||||
@[ @"a", @{kWatchItemConfigKeyPathsPath : @"b", kWatchItemConfigKeyPathsIsPrefix : @(YES)} ],
|
||||
kWatchItemConfigKeyOptions : @{
|
||||
kWatchItemConfigKeyOptionsAllowReadAccess : @(YES),
|
||||
kWatchItemConfigKeyOptionsAuditOnly : @(NO)
|
||||
},
|
||||
kWatchItemConfigKeyProcesses : @[
|
||||
@{kWatchItemConfigKeyProcessesBinaryPath : @"pa"},
|
||||
@{kWatchItemConfigKeyProcessesBinaryPath : @"pb"}
|
||||
]
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertEqual(policies.size(), 2);
|
||||
XCTAssertEqual(*policies[0].get(),
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType, true, false, procs));
|
||||
XCTAssertEqual(*policies[1].get(),
|
||||
WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true, false, procs));
|
||||
}
|
||||
|
||||
- (void)testState {
|
||||
NSString *configPath = @"my_config_path";
|
||||
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
|
||||
|
||||
NSMutableDictionary *config = WrapWatchItemsConfig(@{
|
||||
@"rule1" : @{kWatchItemConfigKeyPaths : @[ @"abc" ]},
|
||||
@"rule2" : @{kWatchItemConfigKeyPaths : @[ @"xyz" ]}
|
||||
});
|
||||
|
||||
WatchItemsPeer watchItems(configPath, NULL, NULL);
|
||||
|
||||
// If no policy yet exists, nullopt is returned
|
||||
std::optional<WatchItemsState> optionalState = watchItems.State();
|
||||
XCTAssertFalse(optionalState.has_value());
|
||||
|
||||
watchItems.ReloadConfig(config);
|
||||
|
||||
optionalState = watchItems.State();
|
||||
XCTAssertTrue(optionalState.has_value());
|
||||
WatchItemsState state = optionalState.value();
|
||||
|
||||
XCTAssertEqual(state.rule_count, [config[kWatchItemConfigKeyWatchItems] count]);
|
||||
XCTAssertCStringEqual(state.policy_version.UTF8String, kVersion.data());
|
||||
XCTAssertEqual(state.config_path, configPath);
|
||||
XCTAssertGreaterThanOrEqual(state.last_config_load_epoch, startTime);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -21,8 +21,9 @@
|
||||
#include <sys/stat.h>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
@@ -49,20 +50,20 @@ class AuthResultCache {
|
||||
AuthResultCache(const AuthResultCache &other) = delete;
|
||||
AuthResultCache &operator=(const AuthResultCache &other) = delete;
|
||||
|
||||
virtual bool AddToCache(const es_file_t *es_file, santa_action_t decision);
|
||||
virtual bool AddToCache(const es_file_t *es_file, SNTAction decision);
|
||||
virtual void RemoveFromCache(const es_file_t *es_file);
|
||||
virtual santa_action_t CheckCache(const es_file_t *es_file);
|
||||
virtual santa_action_t CheckCache(santa_vnode_id_t vnode_id);
|
||||
virtual SNTAction CheckCache(const es_file_t *es_file);
|
||||
virtual SNTAction CheckCache(SantaVnode vnode_id);
|
||||
|
||||
virtual void FlushCache(FlushCacheMode mode);
|
||||
|
||||
virtual NSArray<NSNumber *> *CacheCounts();
|
||||
|
||||
private:
|
||||
virtual SantaCache<santa_vnode_id_t, uint64_t> *CacheForVnodeID(santa_vnode_id_t vnode_id);
|
||||
virtual SantaCache<SantaVnode, uint64_t> *CacheForVnodeID(SantaVnode vnode_id);
|
||||
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *root_cache_;
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *nonroot_cache_;
|
||||
SantaCache<SantaVnode, uint64_t> *root_cache_;
|
||||
SantaCache<SantaVnode, uint64_t> *nonroot_cache_;
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
uint64_t root_devno_;
|
||||
|
||||
@@ -19,37 +19,25 @@
|
||||
#include <time.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SantaVnodeHash.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
|
||||
template <>
|
||||
uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const &t) {
|
||||
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
|
||||
}
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
|
||||
return santa_vnode_id_t{
|
||||
.fsid = (uint64_t)es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline uint64_t GetCurrentUptime() {
|
||||
return clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
}
|
||||
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
static inline uint64_t CacheableAction(santa_action_t action,
|
||||
uint64_t timestamp = GetCurrentUptime()) {
|
||||
static inline uint64_t CacheableAction(SNTAction action, uint64_t timestamp = GetCurrentUptime()) {
|
||||
return ((uint64_t)action << 56) | (timestamp & 0xFFFFFFFFFFFFFF);
|
||||
}
|
||||
|
||||
static inline santa_action_t ActionFromCachedValue(uint64_t cachedValue) {
|
||||
return (santa_action_t)(cachedValue >> 56);
|
||||
static inline SNTAction ActionFromCachedValue(uint64_t cachedValue) {
|
||||
return (SNTAction)(cachedValue >> 56);
|
||||
}
|
||||
|
||||
static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
|
||||
@@ -59,8 +47,8 @@ static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
|
||||
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
root_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
nonroot_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
root_cache_ = new SantaCache<SantaVnode, uint64_t>();
|
||||
nonroot_cache_ = new SantaCache<SantaVnode, uint64_t>();
|
||||
|
||||
struct stat sb;
|
||||
if (stat("/", &sb) == 0) {
|
||||
@@ -78,17 +66,17 @@ AuthResultCache::~AuthResultCache() {
|
||||
delete nonroot_cache_;
|
||||
}
|
||||
|
||||
bool AuthResultCache::AddToCache(const es_file_t *es_file, santa_action_t decision) {
|
||||
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
bool AuthResultCache::AddToCache(const es_file_t *es_file, SNTAction decision) {
|
||||
SantaVnode vnode_id = SantaVnode::VnodeForFile(es_file);
|
||||
SantaCache<SantaVnode, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
switch (decision) {
|
||||
case ACTION_REQUEST_BINARY:
|
||||
return cache->set(vnode_id, CacheableAction(ACTION_REQUEST_BINARY, 0), 0);
|
||||
case ACTION_RESPOND_ALLOW: OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_ALLOW_COMPILER: OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_DENY:
|
||||
case SNTActionRequestBinary:
|
||||
return cache->set(vnode_id, CacheableAction(SNTActionRequestBinary, 0), 0);
|
||||
case SNTActionRespondAllow: OS_FALLTHROUGH;
|
||||
case SNTActionRespondAllowCompiler: OS_FALLTHROUGH;
|
||||
case SNTActionRespondDeny:
|
||||
return cache->set(vnode_id, CacheableAction(decision),
|
||||
CacheableAction(ACTION_REQUEST_BINARY, 0));
|
||||
CacheableAction(SNTActionRequestBinary, 0));
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid cache value, exiting.");
|
||||
@@ -97,37 +85,36 @@ bool AuthResultCache::AddToCache(const es_file_t *es_file, santa_action_t decisi
|
||||
}
|
||||
|
||||
void AuthResultCache::RemoveFromCache(const es_file_t *es_file) {
|
||||
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
|
||||
SantaVnode vnode_id = SantaVnode::VnodeForFile(es_file);
|
||||
CacheForVnodeID(vnode_id)->remove(vnode_id);
|
||||
}
|
||||
|
||||
santa_action_t AuthResultCache::CheckCache(const es_file_t *es_file) {
|
||||
return CheckCache(VnodeForFile(es_file));
|
||||
SNTAction AuthResultCache::CheckCache(const es_file_t *es_file) {
|
||||
return CheckCache(SantaVnode::VnodeForFile(es_file));
|
||||
}
|
||||
|
||||
santa_action_t AuthResultCache::CheckCache(santa_vnode_id_t vnode_id) {
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
SNTAction AuthResultCache::CheckCache(SantaVnode vnode_id) {
|
||||
SantaCache<SantaVnode, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
|
||||
uint64_t cached_val = cache->get(vnode_id);
|
||||
if (cached_val == 0) {
|
||||
return ACTION_UNSET;
|
||||
return SNTActionUnset;
|
||||
}
|
||||
|
||||
santa_action_t result = ActionFromCachedValue(cached_val);
|
||||
SNTAction result = ActionFromCachedValue(cached_val);
|
||||
|
||||
if (result == ACTION_RESPOND_DENY) {
|
||||
if (result == SNTActionRespondDeny) {
|
||||
uint64_t expiry_time = TimestampFromCachedValue(cached_val) + cache_deny_time_ns_;
|
||||
if (expiry_time < GetCurrentUptime()) {
|
||||
cache->remove(vnode_id);
|
||||
return ACTION_UNSET;
|
||||
return SNTActionUnset;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *AuthResultCache::CacheForVnodeID(
|
||||
santa_vnode_id_t vnode_id) {
|
||||
SantaCache<SantaVnode, uint64_t> *AuthResultCache::CacheForVnodeID(SantaVnode vnode_id) {
|
||||
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
#include "Source/common/SantaVnode.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
@@ -47,13 +47,6 @@ static inline es_file_t MakeCacheableFile(uint64_t devno, uint64_t ino) {
|
||||
.path = {}, .path_truncated = false, .stat = {.st_dev = (dev_t)devno, .st_ino = ino}};
|
||||
}
|
||||
|
||||
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
|
||||
return santa_vnode_id_t{
|
||||
.fsid = (uint64_t)es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uint64_t root_count,
|
||||
uint64_t nonroot_count) {
|
||||
NSArray<NSNumber *> *counts = cache->CacheCounts();
|
||||
@@ -86,33 +79,33 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
|
||||
|
||||
// Add the root file to the cache
|
||||
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
|
||||
cache->AddToCache(&rootFile, SNTActionRequestBinary);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_UNSET);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRequestBinary);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), SNTActionUnset);
|
||||
|
||||
// Now add the non-root file
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
cache->AddToCache(&nonrootFile, SNTActionRequestBinary);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRequestBinary);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), SNTActionRequestBinary);
|
||||
|
||||
// Update the cached values
|
||||
cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW);
|
||||
cache->AddToCache(&nonrootFile, ACTION_RESPOND_DENY);
|
||||
cache->AddToCache(&rootFile, SNTActionRespondAllow);
|
||||
cache->AddToCache(&nonrootFile, SNTActionRespondDeny);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
XCTAssertEqual(cache->CheckCache(VnodeForFile(&rootFile)), ACTION_RESPOND_ALLOW);
|
||||
XCTAssertEqual(cache->CheckCache(VnodeForFile(&nonrootFile)), ACTION_RESPOND_DENY);
|
||||
XCTAssertEqual(cache->CheckCache(SantaVnode::VnodeForFile(&rootFile)), SNTActionRespondAllow);
|
||||
XCTAssertEqual(cache->CheckCache(SantaVnode::VnodeForFile(&nonrootFile)), SNTActionRespondDeny);
|
||||
|
||||
// Remove the root file
|
||||
cache->RemoveFromCache(&rootFile);
|
||||
|
||||
AssertCacheCounts(cache, 0, 1);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_RESPOND_DENY);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionUnset);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), SNTActionRespondDeny);
|
||||
}
|
||||
|
||||
- (void)testFlushCache {
|
||||
@@ -122,8 +115,8 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
|
||||
|
||||
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
cache->AddToCache(&rootFile, SNTActionRequestBinary);
|
||||
cache->AddToCache(&nonrootFile, SNTActionRequestBinary);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
@@ -133,7 +126,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
// Add back the non-root file
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
cache->AddToCache(&nonrootFile, SNTActionRequestBinary);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
@@ -162,33 +155,33 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
// Cached items must first be in the ACTION_REQUEST_BINARY state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW_COMPILER));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
// Cached items must first be in the SNTActionRequestBinary state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, SNTActionRespondAllow));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, SNTActionRespondAllowCompiler));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, SNTActionRespondDeny));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionUnset);
|
||||
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, SNTActionRequestBinary));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRequestBinary);
|
||||
|
||||
// Items in the `ACTION_REQUEST_BINARY` state cannot reenter the same state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
// Items in the `SNTActionRequestBinary` state cannot reenter the same state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, SNTActionRequestBinary));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRequestBinary);
|
||||
|
||||
santa_action_t allowed_transitions[] = {
|
||||
ACTION_RESPOND_ALLOW,
|
||||
ACTION_RESPOND_ALLOW_COMPILER,
|
||||
ACTION_RESPOND_DENY,
|
||||
SNTAction allowed_transitions[] = {
|
||||
SNTActionRespondAllow,
|
||||
SNTActionRespondAllowCompiler,
|
||||
SNTActionRespondDeny,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(allowed_transitions) / sizeof(allowed_transitions[0]); i++) {
|
||||
// First make sure the item doesn't exist
|
||||
cache->RemoveFromCache(&rootFile);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionUnset);
|
||||
|
||||
// Now add the item to be in the first allowed state
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, SNTActionRequestBinary));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRequestBinary);
|
||||
|
||||
// Now assert the allowed transition
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, allowed_transitions[i]));
|
||||
@@ -204,12 +197,12 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
// Add a file to the cache and put into the ACTION_RESPOND_DENY state
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
|
||||
// Add a file to the cache and put into the SNTActionRespondDeny state
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, SNTActionRequestBinary));
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, SNTActionRespondDeny));
|
||||
|
||||
// Ensure the file exists
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_RESPOND_DENY);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRespondDeny);
|
||||
|
||||
// Wait for the item to expire
|
||||
SleepMS(expiryMS);
|
||||
@@ -218,7 +211,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
// Now check the cache, which will remove the item
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionUnset);
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <set>
|
||||
#include <string_view>
|
||||
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
@@ -32,12 +34,26 @@ class EndpointSecurityAPI : public std::enable_shared_from_this<EndpointSecurity
|
||||
virtual Client NewClient(void (^message_handler)(es_client_t *, Message));
|
||||
|
||||
virtual bool Subscribe(const Client &client, const std::set<es_event_type_t> &);
|
||||
virtual bool UnsubscribeAll(const Client &client);
|
||||
|
||||
virtual es_message_t *RetainMessage(const es_message_t *msg);
|
||||
virtual void ReleaseMessage(es_message_t *msg);
|
||||
virtual bool UnmuteAllPaths(const Client &client);
|
||||
virtual bool UnmuteAllTargetPaths(const Client &client);
|
||||
|
||||
virtual bool IsTargetPathMutingInverted(const Client &client);
|
||||
virtual bool InvertTargetPathMuting(const Client &client);
|
||||
|
||||
virtual bool MuteTargetPath(const Client &client, std::string_view path,
|
||||
santa::santad::data_layer::WatchItemPathType path_type);
|
||||
virtual bool UnmuteTargetPath(const Client &client, std::string_view path,
|
||||
santa::santad::data_layer::WatchItemPathType path_type);
|
||||
|
||||
virtual void RetainMessage(const es_message_t *msg);
|
||||
virtual void ReleaseMessage(const es_message_t *msg);
|
||||
|
||||
virtual bool RespondAuthResult(const Client &client, const Message &msg, es_auth_result_t result,
|
||||
bool cache);
|
||||
virtual bool RespondFlagsResult(const Client &client, const Message &msg, uint32_t allowed_flags,
|
||||
bool cache);
|
||||
|
||||
virtual bool MuteProcess(const Client &client, const audit_token_t *tok);
|
||||
|
||||
|
||||
@@ -13,11 +13,14 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/common/Platform.h"
|
||||
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Client EndpointSecurityAPI::NewClient(void (^message_handler)(es_client_t *, Message)) {
|
||||
@@ -33,28 +36,12 @@ Client EndpointSecurityAPI::NewClient(void (^message_handler)(es_client_t *, Mes
|
||||
return Client(client, res);
|
||||
}
|
||||
|
||||
es_message_t *EndpointSecurityAPI::RetainMessage(const es_message_t *msg) {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
es_retain_message(msg);
|
||||
es_message_t *nonconst = const_cast<es_message_t *>(msg);
|
||||
return nonconst;
|
||||
} else {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return es_copy_message(msg);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
void EndpointSecurityAPI::RetainMessage(const es_message_t *msg) {
|
||||
es_retain_message(msg);
|
||||
}
|
||||
|
||||
void EndpointSecurityAPI::ReleaseMessage(es_message_t *msg) {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
es_release_message(msg);
|
||||
} else {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return es_free_message(msg);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
void EndpointSecurityAPI::ReleaseMessage(const es_message_t *msg) {
|
||||
es_release_message(msg);
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::Subscribe(const Client &client,
|
||||
@@ -63,11 +50,87 @@ bool EndpointSecurityAPI::Subscribe(const Client &client,
|
||||
return es_subscribe(client.Get(), subs.data(), (uint32_t)subs.size()) == ES_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::UnsubscribeAll(const Client &client) {
|
||||
return es_unsubscribe_all(client.Get()) == ES_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::UnmuteAllPaths(const Client &client) {
|
||||
return es_unmute_all_paths(client.Get()) == ES_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::UnmuteAllTargetPaths(const Client &client) {
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.0, *)) {
|
||||
return es_unmute_all_target_paths(client.Get()) == ES_RETURN_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::IsTargetPathMutingInverted(const Client &client) {
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.0, *)) {
|
||||
return es_muting_inverted(client.Get(), ES_MUTE_INVERSION_TYPE_TARGET_PATH) == ES_MUTE_INVERTED;
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::InvertTargetPathMuting(const Client &client) {
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.0, *)) {
|
||||
if (!IsTargetPathMutingInverted(client)) {
|
||||
return es_invert_muting(client.Get(), ES_MUTE_INVERSION_TYPE_TARGET_PATH) ==
|
||||
ES_RETURN_SUCCESS;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::MuteTargetPath(const Client &client, std::string_view path,
|
||||
WatchItemPathType path_type) {
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.0, *)) {
|
||||
return es_mute_path(client.Get(), path.data(),
|
||||
path_type == WatchItemPathType::kPrefix
|
||||
? ES_MUTE_PATH_TYPE_TARGET_PREFIX
|
||||
: ES_MUTE_PATH_TYPE_TARGET_LITERAL) == ES_RETURN_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::UnmuteTargetPath(const Client &client, std::string_view path,
|
||||
WatchItemPathType path_type) {
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.0, *)) {
|
||||
return es_unmute_path(client.Get(), path.data(),
|
||||
path_type == WatchItemPathType::kPrefix
|
||||
? ES_MUTE_PATH_TYPE_TARGET_PREFIX
|
||||
: ES_MUTE_PATH_TYPE_TARGET_LITERAL) == ES_RETURN_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::RespondAuthResult(const Client &client, const Message &msg,
|
||||
es_auth_result_t result, bool cache) {
|
||||
return es_respond_auth_result(client.Get(), &(*msg), result, cache) == ES_RESPOND_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::RespondFlagsResult(const Client &client, const Message &msg,
|
||||
uint32_t allowed_flags, bool cache) {
|
||||
return es_respond_flags_result(client.Get(), &(*msg), allowed_flags, cache);
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::MuteProcess(const Client &client, const audit_token_t *tok) {
|
||||
return es_mute_process(client.Get(), tok) == ES_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -21,16 +21,30 @@
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
enum class EnrichOptions {
|
||||
// Specifies default enricher operation.
|
||||
kDefault,
|
||||
|
||||
// This option tells the enricher to only enrich with information that can be
|
||||
// gathered without potentially triggering work from external processes.
|
||||
kLocalOnly,
|
||||
};
|
||||
|
||||
class Enricher {
|
||||
public:
|
||||
Enricher();
|
||||
virtual ~Enricher() = default;
|
||||
virtual std::shared_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual EnrichedProcess Enrich(const es_process_t &es_proc);
|
||||
virtual EnrichedFile Enrich(const es_file_t &es_file);
|
||||
virtual EnrichedProcess Enrich(
|
||||
const es_process_t &es_proc,
|
||||
EnrichOptions options = EnrichOptions::kDefault);
|
||||
virtual EnrichedFile Enrich(const es_file_t &es_file,
|
||||
EnrichOptions options = EnrichOptions::kDefault);
|
||||
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForUID(uid_t uid);
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForGID(gid_t gid);
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForUID(
|
||||
uid_t uid, EnrichOptions options = EnrichOptions::kDefault);
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForGID(
|
||||
gid_t gid, EnrichOptions options = EnrichOptions::kDefault);
|
||||
|
||||
private:
|
||||
SantaCache<uid_t, std::optional<std::shared_ptr<std::string>>>
|
||||
|
||||
@@ -81,26 +81,30 @@ std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
}
|
||||
}
|
||||
|
||||
EnrichedProcess Enricher::Enrich(const es_process_t &es_proc) {
|
||||
return EnrichedProcess(UsernameForUID(audit_token_to_euid(es_proc.audit_token)),
|
||||
UsernameForGID(audit_token_to_egid(es_proc.audit_token)),
|
||||
UsernameForUID(audit_token_to_ruid(es_proc.audit_token)),
|
||||
UsernameForGID(audit_token_to_rgid(es_proc.audit_token)),
|
||||
Enrich(*es_proc.executable));
|
||||
EnrichedProcess Enricher::Enrich(const es_process_t &es_proc, EnrichOptions options) {
|
||||
return EnrichedProcess(UsernameForUID(audit_token_to_euid(es_proc.audit_token), options),
|
||||
UsernameForGID(audit_token_to_egid(es_proc.audit_token), options),
|
||||
UsernameForUID(audit_token_to_ruid(es_proc.audit_token), options),
|
||||
UsernameForGID(audit_token_to_rgid(es_proc.audit_token), options),
|
||||
Enrich(*es_proc.executable, options));
|
||||
}
|
||||
|
||||
EnrichedFile Enricher::Enrich(const es_file_t &es_file) {
|
||||
EnrichedFile Enricher::Enrich(const es_file_t &es_file, EnrichOptions options) {
|
||||
// TODO(mlw): Consider having the enricher perform file hashing. This will
|
||||
// make more sense if we start including hashes in more event types.
|
||||
return EnrichedFile(UsernameForUID(es_file.stat.st_uid), UsernameForGID(es_file.stat.st_gid),
|
||||
std::nullopt);
|
||||
return EnrichedFile(UsernameForUID(es_file.stat.st_uid, options),
|
||||
UsernameForGID(es_file.stat.st_gid, options), std::nullopt);
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForUID(uid_t uid) {
|
||||
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForUID(uid_t uid,
|
||||
EnrichOptions options) {
|
||||
std::optional<std::shared_ptr<std::string>> username = username_cache_.get(uid);
|
||||
|
||||
if (username.has_value()) {
|
||||
return username;
|
||||
} else if (options == EnrichOptions::kLocalOnly) {
|
||||
// If `kLocalOnly` option is set, do not attempt a lookup
|
||||
return std::nullopt;
|
||||
} else {
|
||||
struct passwd *pw = getpwuid(uid);
|
||||
if (pw) {
|
||||
@@ -115,11 +119,15 @@ std::optional<std::shared_ptr<std::string>> Enricher::UsernameForUID(uid_t uid)
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForGID(gid_t gid) {
|
||||
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForGID(gid_t gid,
|
||||
EnrichOptions options) {
|
||||
std::optional<std::shared_ptr<std::string>> groupname = groupname_cache_.get(gid);
|
||||
|
||||
if (groupname.has_value()) {
|
||||
return groupname;
|
||||
} else if (options == EnrichOptions::kLocalOnly) {
|
||||
// If `kLocalOnly` option is set, do not attempt a lookup
|
||||
return std::nullopt;
|
||||
} else {
|
||||
struct group *gr = getgrgid(gid);
|
||||
if (gr) {
|
||||
|
||||
@@ -34,8 +34,6 @@ class Message {
|
||||
// Note: Safe to implement this, just not currently needed so left deleted.
|
||||
Message& operator=(Message&& rhs) = delete;
|
||||
|
||||
// In macOS 10.15, es_retain_message/es_release_message were unsupported
|
||||
// and required a full copy, which impacts performance if done too much...
|
||||
Message(const Message& other);
|
||||
Message& operator=(const Message& other) = delete;
|
||||
|
||||
@@ -47,10 +45,7 @@ class Message {
|
||||
|
||||
private:
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi_;
|
||||
es_message_t* es_msg_;
|
||||
|
||||
mutable std::string pname_;
|
||||
mutable std::string parent_pname_;
|
||||
const es_message_t* es_msg_;
|
||||
|
||||
std::string GetProcessName(pid_t pid) const;
|
||||
};
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
|
||||
: esapi_(esapi) {
|
||||
es_msg_ = esapi_->RetainMessage(es_msg);
|
||||
: esapi_(std::move(esapi)), es_msg_(es_msg) {
|
||||
esapi_->RetainMessage(es_msg);
|
||||
}
|
||||
|
||||
Message::~Message() {
|
||||
@@ -45,10 +45,7 @@ Message::Message(const Message &other) {
|
||||
}
|
||||
|
||||
std::string Message::ParentProcessName() const {
|
||||
if (parent_pname_.length() == 0) {
|
||||
parent_pname_ = GetProcessName(es_msg_->process->ppid);
|
||||
}
|
||||
return parent_pname_;
|
||||
return GetProcessName(es_msg_->process->ppid);
|
||||
}
|
||||
|
||||
std::string Message::GetProcessName(pid_t pid) const {
|
||||
|
||||
@@ -65,7 +65,7 @@ pid_t AttemptToFindUnusedPID() {
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
// Constructing a `Message` retains the underlying `es_message_t` and it is
|
||||
// released when the `Message` object is destructed.
|
||||
@@ -82,9 +82,7 @@ pid_t AttemptToFindUnusedPID() {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
EXPECT_CALL(*mockESApi, ReleaseMessage(testing::_))
|
||||
.Times(2)
|
||||
.After(EXPECT_CALL(*mockESApi, RetainMessage(testing::_))
|
||||
.Times(2)
|
||||
.WillRepeatedly(testing::Return(&esMsg)));
|
||||
.After(EXPECT_CALL(*mockESApi, RetainMessage(testing::_)).Times(2));
|
||||
|
||||
{
|
||||
Message msg1(mockESApi, &esMsg);
|
||||
@@ -106,7 +104,7 @@ pid_t AttemptToFindUnusedPID() {
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
// Search for an *existing* parent process.
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
@@ -38,14 +39,32 @@ class MockEndpointSecurityAPI
|
||||
MOCK_METHOD(bool, Subscribe,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &,
|
||||
const std::set<es_event_type_t> &));
|
||||
MOCK_METHOD(bool, UnsubscribeAll, (const Client &client));
|
||||
|
||||
MOCK_METHOD(es_message_t *, RetainMessage, (const es_message_t *msg));
|
||||
MOCK_METHOD(void, ReleaseMessage, (es_message_t * msg));
|
||||
MOCK_METHOD(bool, UnmuteAllPaths, (const Client &client));
|
||||
MOCK_METHOD(bool, UnmuteAllTargetPaths, (const Client &client));
|
||||
|
||||
MOCK_METHOD(bool, IsTargetPathMutingInverted, (const Client &client));
|
||||
MOCK_METHOD(bool, InvertTargetPathMuting, (const Client &client));
|
||||
|
||||
MOCK_METHOD(bool, MuteTargetPath,
|
||||
(const Client &client, std::string_view path,
|
||||
santa::santad::data_layer::WatchItemPathType path_type));
|
||||
MOCK_METHOD(bool, UnmuteTargetPath,
|
||||
(const Client &client, std::string_view path,
|
||||
santa::santad::data_layer::WatchItemPathType path_type));
|
||||
|
||||
MOCK_METHOD(void, RetainMessage, (const es_message_t *msg));
|
||||
MOCK_METHOD(void, ReleaseMessage, (const es_message_t *msg));
|
||||
|
||||
MOCK_METHOD(bool, RespondAuthResult,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
es_auth_result_t result, bool cache));
|
||||
MOCK_METHOD(bool, RespondFlagsResult,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &client,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
uint32_t allowed_flags, bool cache));
|
||||
|
||||
MOCK_METHOD(bool, MuteProcess,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &,
|
||||
@@ -72,9 +91,9 @@ class MockEndpointSecurityAPI
|
||||
EXPECT_CALL(*this, Subscribe).WillRepeatedly(testing::Return(true));
|
||||
}
|
||||
|
||||
void SetExpectationsRetainReleaseMessage(es_message_t *msg) {
|
||||
void SetExpectationsRetainReleaseMessage() {
|
||||
EXPECT_CALL(*this, ReleaseMessage).Times(testing::AnyNumber());
|
||||
EXPECT_CALL(*this, RetainMessage).WillRepeatedly(testing::Return(msg));
|
||||
EXPECT_CALL(*this, RetainMessage).Times(testing::AnyNumber());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include <os/base.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#import "Source/common/BranchPrediction.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
@@ -64,15 +66,15 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
const es_file_t *targetFile = msg->event.exec.target->executable;
|
||||
|
||||
while (true) {
|
||||
santa_action_t returnAction = self->_authResultCache->CheckCache(targetFile);
|
||||
SNTAction returnAction = self->_authResultCache->CheckCache(targetFile);
|
||||
if (RESPONSE_VALID(returnAction)) {
|
||||
es_auth_result_t authResult = ES_AUTH_RESULT_DENY;
|
||||
|
||||
switch (returnAction) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
case SNTActionRespondAllowCompiler:
|
||||
[self.compilerController setProcess:msg->event.exec.target->audit_token isCompiler:true];
|
||||
OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_ALLOW: authResult = ES_AUTH_RESULT_ALLOW; break;
|
||||
case SNTActionRespondAllow: authResult = ES_AUTH_RESULT_ALLOW; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
@@ -80,7 +82,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
withAuthResult:authResult
|
||||
cacheable:(authResult == ES_AUTH_RESULT_ALLOW)];
|
||||
return;
|
||||
} else if (returnAction == ACTION_REQUEST_BINARY) {
|
||||
} else if (returnAction == SNTActionRequestBinary) {
|
||||
// TODO(mlw): Add a metric here to observe how ofthen this happens in practice.
|
||||
// TODO(mlw): Look into caching a `Deferred<value>` to better prevent
|
||||
// raciness of multiple threads checking the cache simultaneously.
|
||||
@@ -91,10 +93,10 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
}
|
||||
}
|
||||
|
||||
self->_authResultCache->AddToCache(targetFile, ACTION_REQUEST_BINARY);
|
||||
self->_authResultCache->AddToCache(targetFile, SNTActionRequestBinary);
|
||||
|
||||
[self.execController validateExecEvent:msg
|
||||
postAction:^bool(santa_action_t action) {
|
||||
postAction:^bool(SNTAction action) {
|
||||
return [self postAction:action forMessage:msg];
|
||||
}];
|
||||
}
|
||||
@@ -109,7 +111,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
}
|
||||
|
||||
if (![self.execController synchronousShouldProcessExecEvent:esMsg]) {
|
||||
[self postAction:ACTION_RESPOND_DENY forMessage:esMsg];
|
||||
[self postAction:SNTActionRespondDeny forMessage:esMsg];
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
@@ -121,19 +123,19 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
}];
|
||||
}
|
||||
|
||||
- (bool)postAction:(santa_action_t)action forMessage:(const Message &)esMsg {
|
||||
- (bool)postAction:(SNTAction)action forMessage:(const Message &)esMsg {
|
||||
es_auth_result_t authResult;
|
||||
|
||||
switch (action) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
case SNTActionRespondAllowCompiler:
|
||||
[self.compilerController setProcess:esMsg->event.exec.target->audit_token isCompiler:true];
|
||||
OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_ALLOW: authResult = ES_AUTH_RESULT_ALLOW; break;
|
||||
case ACTION_RESPOND_DENY: authResult = ES_AUTH_RESULT_DENY; break;
|
||||
case SNTActionRespondAllow: authResult = ES_AUTH_RESULT_ALLOW; break;
|
||||
case SNTActionRespondDeny: authResult = ES_AUTH_RESULT_DENY; break;
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid action for postAction, exiting.");
|
||||
[NSException raise:@"Invalid post action" format:@"Invalid post action: %d", action];
|
||||
[NSException raise:@"Invalid post action" format:@"Invalid post action: %ld", action];
|
||||
}
|
||||
|
||||
self->_authResultCache->AddToCache(esMsg->event.exec.target->executable, action);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
@@ -40,13 +41,13 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
public:
|
||||
using AuthResultCache::AuthResultCache;
|
||||
|
||||
MOCK_METHOD(bool, AddToCache, (const es_file_t *es_file, santa_action_t decision));
|
||||
MOCK_METHOD(santa_action_t, CheckCache, (const es_file_t *es_file));
|
||||
MOCK_METHOD(bool, AddToCache, (const es_file_t *es_file, SNTAction decision));
|
||||
MOCK_METHOD(SNTAction, CheckCache, (const es_file_t *es_file));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityAuthorizer (Testing)
|
||||
- (void)processMessage:(const Message &)msg;
|
||||
- (bool)postAction:(santa_action_t)action forMessage:(const Message &)esMsg;
|
||||
- (bool)postAction:(SNTAction)action forMessage:(const Message &)esMsg;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityAuthorizerTest : XCTestCase
|
||||
@@ -90,7 +91,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
// There is a benign leak of the mock object in this test.
|
||||
// `handleMessage:recordEventMetrics:` will call `processMessage:handler:` in the parent
|
||||
@@ -129,9 +130,10 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(NO);
|
||||
|
||||
OCMExpect([mockAuthClient postAction:ACTION_RESPOND_DENY forMessage:Message(mockESApi, &esMsg)])
|
||||
OCMExpect([mockAuthClient postAction:SNTActionRespondDeny
|
||||
forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient postAction:ACTION_RESPOND_DENY forMessage:Message(mockESApi, &esMsg)])
|
||||
OCMStub([mockAuthClient postAction:SNTActionRespondDeny forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
|
||||
@@ -186,15 +188,15 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, CheckCache)
|
||||
.WillOnce(testing::Return(ACTION_REQUEST_BINARY))
|
||||
.WillOnce(testing::Return(ACTION_REQUEST_BINARY))
|
||||
.WillOnce(testing::Return(ACTION_RESPOND_ALLOW_COMPILER))
|
||||
.WillOnce(testing::Return(ACTION_UNSET));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(testing::_, ACTION_REQUEST_BINARY))
|
||||
.WillOnce(testing::Return(SNTActionRequestBinary))
|
||||
.WillOnce(testing::Return(SNTActionRequestBinary))
|
||||
.WillOnce(testing::Return(SNTActionRespondAllowCompiler))
|
||||
.WillOnce(testing::Return(SNTActionUnset));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(testing::_, SNTActionRequestBinary))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
id mockCompilerController = OCMStrictClassMock([SNTCompilerController class]);
|
||||
@@ -210,7 +212,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
// This block tests that processing is held up until an outstanding thread
|
||||
// processing another event completes and returns a result. This test
|
||||
// specifically will check the `ACTION_RESPOND_ALLOW_COMPILER` flow.
|
||||
// specifically will check the `SNTActionRespondAllowCompiler` flow.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
OCMExpect([mockAuthClient respondToMessage:msg
|
||||
@@ -253,14 +255,14 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_ALLOW_COMPILER))
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllowCompiler))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_ALLOW))
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllow))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_DENY))
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondDeny))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
id mockCompilerController = OCMStrictClassMock([SNTCompilerController class]);
|
||||
@@ -277,12 +279,12 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
XCTAssertThrows([mockAuthClient postAction:(santa_action_t)123 forMessage:msg]);
|
||||
XCTAssertThrows([mockAuthClient postAction:(SNTAction)123 forMessage:msg]);
|
||||
|
||||
std::map<santa_action_t, es_auth_result_t> actions = {
|
||||
{ACTION_RESPOND_ALLOW_COMPILER, ES_AUTH_RESULT_ALLOW},
|
||||
{ACTION_RESPOND_ALLOW, ES_AUTH_RESULT_ALLOW},
|
||||
{ACTION_RESPOND_DENY, ES_AUTH_RESULT_DENY},
|
||||
std::map<SNTAction, es_auth_result_t> actions = {
|
||||
{SNTActionRespondAllowCompiler, ES_AUTH_RESULT_ALLOW},
|
||||
{SNTActionRespondAllow, ES_AUTH_RESULT_ALLOW},
|
||||
{SNTActionRespondDeny, ES_AUTH_RESULT_DENY},
|
||||
};
|
||||
|
||||
for (const auto &kv : actions) {
|
||||
|
||||
@@ -21,9 +21,15 @@
|
||||
#include <stdlib.h>
|
||||
#include <sys/qos.h>
|
||||
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "Source/common/BranchPrediction.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/common/SystemResources.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
@@ -32,11 +38,15 @@
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::Metrics;
|
||||
using santa::santad::Processor;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db",
|
||||
"/private/var/db/santa/events.db"};
|
||||
|
||||
@interface SNTEndpointSecurityClient ()
|
||||
@property int64_t deadlineMarginMS;
|
||||
@end
|
||||
@@ -45,7 +55,6 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
std::shared_ptr<EndpointSecurityAPI> _esApi;
|
||||
std::shared_ptr<Metrics> _metrics;
|
||||
Client _esClient;
|
||||
mach_timebase_info_data_t _timebase;
|
||||
dispatch_queue_t _authQueue;
|
||||
dispatch_queue_t _notifyQueue;
|
||||
Processor _processor;
|
||||
@@ -61,12 +70,6 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
_deadlineMarginMS = 5000;
|
||||
_processor = processor;
|
||||
|
||||
if (mach_timebase_info(&_timebase) != KERN_SUCCESS) {
|
||||
LOGE(@"Failed to get mach timebase info");
|
||||
// Assumed to be transitory failure. Let the daemon restart.
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
_authQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.auth_queue",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
|
||||
@@ -187,16 +190,65 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
return [self subscribe:events] && [self clearCache];
|
||||
}
|
||||
|
||||
- (bool)unsubscribeAll {
|
||||
return _esApi->UnsubscribeAll(_esClient);
|
||||
}
|
||||
|
||||
- (bool)unmuteEverything {
|
||||
bool result = _esApi->UnmuteAllPaths(_esClient);
|
||||
result = _esApi->UnmuteAllTargetPaths(_esClient) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (bool)enableTargetPathWatching {
|
||||
return _esApi->InvertTargetPathMuting(_esClient);
|
||||
}
|
||||
|
||||
- (bool)muteTargetPaths:(const std::vector<std::pair<std::string, WatchItemPathType>> &)paths {
|
||||
bool result = true;
|
||||
for (const auto &pathAndTypePair : paths) {
|
||||
result =
|
||||
_esApi->MuteTargetPath(_esClient, pathAndTypePair.first, pathAndTypePair.second) && result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (bool)unmuteTargetPaths:(const std::vector<std::pair<std::string, WatchItemPathType>> &)paths {
|
||||
bool result = true;
|
||||
for (const auto &pathAndTypePair : paths) {
|
||||
result =
|
||||
_esApi->UnmuteTargetPath(_esClient, pathAndTypePair.first, pathAndTypePair.second) && result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (bool)respondToMessage:(const Message &)msg
|
||||
withAuthResult:(es_auth_result_t)result
|
||||
cacheable:(bool)cacheable {
|
||||
return _esApi->RespondAuthResult(_esClient, msg, result, cacheable);
|
||||
if (msg->event_type == ES_EVENT_TYPE_AUTH_OPEN) {
|
||||
return _esApi->RespondFlagsResult(
|
||||
// For now, Santa is only concerned about alllowing all access or no
|
||||
// access, hence the flags being translated here to all or nothing based
|
||||
// on the auth result. In the future it might be beneficial to expand the
|
||||
// scope of Santa to enforce things like read-only access.
|
||||
_esClient, msg, (result == ES_AUTH_RESULT_ALLOW) ? 0xffffffff : 0x0, cacheable);
|
||||
} else {
|
||||
return _esApi->RespondAuthResult(_esClient, msg, result, cacheable);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)processEnrichedMessage:(std::shared_ptr<EnrichedMessage>)msg
|
||||
handler:(void (^)(std::shared_ptr<EnrichedMessage>))messageHandler {
|
||||
__block std::shared_ptr<EnrichedMessage> msgTmp = std::move(msg);
|
||||
dispatch_async(_notifyQueue, ^{
|
||||
messageHandler(std::move(msg));
|
||||
messageHandler(std::move(msgTmp));
|
||||
});
|
||||
}
|
||||
|
||||
- (void)asynchronouslyProcess:(Message)msg handler:(void (^)(Message &&))messageHandler {
|
||||
__block Message msgTmp = std::move(msg);
|
||||
dispatch_async(_notifyQueue, ^{
|
||||
messageHandler(std::move(msgTmp));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -216,15 +268,14 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
|
||||
|
||||
const uint64_t timeout = NSEC_PER_MSEC * (self.deadlineMarginMS);
|
||||
uint64_t deadlineMachTime = msg->deadline - mach_absolute_time();
|
||||
uint64_t deadlineNano = deadlineMachTime * _timebase.numer / _timebase.denom;
|
||||
|
||||
uint64_t deadlineNano = MachTimeToNanos(msg->deadline - mach_absolute_time());
|
||||
|
||||
// TODO(mlw): How should we handle `deadlineNano <= timeout`. Will currently
|
||||
// result in the deadline block being dispatched immediately (and therefore
|
||||
// the event will be denied).
|
||||
|
||||
// Workaround for compiler bug that doesn't properly close over variables
|
||||
// Note: On macOS 10.15 this will cause extra message copies.
|
||||
__block Message processMsg = msg;
|
||||
__block Message deadlineMsg = msg;
|
||||
|
||||
@@ -245,7 +296,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
});
|
||||
|
||||
dispatch_async(self->_authQueue, ^{
|
||||
messageHandler(deadlineMsg);
|
||||
messageHandler(processMsg);
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Deadline expired, wait for deadline block to finish.
|
||||
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
|
||||
@@ -253,12 +304,28 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
});
|
||||
}
|
||||
|
||||
+ (bool)isDatabasePath:(const std::string_view)path {
|
||||
+ (std::set<std::string>)getProtectedPaths {
|
||||
std::set<std::string> protectedPathsCopy;
|
||||
|
||||
for (size_t i = 0; i < sizeof(kProtectedFiles) / sizeof(kProtectedFiles[0]); i++) {
|
||||
protectedPathsCopy.insert(std::string(kProtectedFiles[i]));
|
||||
}
|
||||
|
||||
return protectedPathsCopy;
|
||||
}
|
||||
|
||||
+ (bool)isProtectedPath:(const std::string_view)path {
|
||||
// TODO(mlw): These values should come from `SNTDatabaseController`. But right
|
||||
// now they live as NSStrings. We should make them `std::string_view` types
|
||||
// in order to use them here efficiently, but will need to make the
|
||||
// `SNTDatabaseController` an ObjC++ file.
|
||||
return (path == "/private/var/db/santa/rules.db" || path == "/private/var/db/santa/events.db");
|
||||
for (size_t i = 0; i < sizeof(kProtectedFiles) / sizeof(kProtectedFiles[0]); i++) {
|
||||
if (path == kProtectedFiles[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
@@ -46,12 +48,24 @@
|
||||
/// subscribing mitigates this posibility.
|
||||
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events;
|
||||
|
||||
- (bool)unsubscribeAll;
|
||||
- (bool)unmuteEverything;
|
||||
- (bool)enableTargetPathWatching;
|
||||
- (bool)muteTargetPaths:
|
||||
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>> &)paths;
|
||||
- (bool)unmuteTargetPaths:
|
||||
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>> &)paths;
|
||||
|
||||
/// Responds to the Message with the given auth result
|
||||
///
|
||||
/// @param Message The wrapped es_message_t being responded to
|
||||
/// @param result Either ES_AUTH_RESULT_ALLOW or ES_AUTH_RESULT_DENY
|
||||
/// @param cacheable true if ES should attempt to cache the result, otherwise false
|
||||
/// @return true if the response was successful, otherwise false
|
||||
///
|
||||
/// @note If the msg event type requires a flags response, the correct ES API will automatically
|
||||
/// be called. ALLOWED results will be translated to having all flags set, and DENIED results
|
||||
/// will be translated to having all flags cleared.
|
||||
- (bool)respondToMessage:(const santa::santad::event_providers::endpoint_security::Message &)msg
|
||||
withAuthResult:(es_auth_result_t)result
|
||||
cacheable:(bool)cacheable;
|
||||
@@ -64,6 +78,11 @@
|
||||
santa::santad::event_providers::endpoint_security::EnrichedMessage>))
|
||||
messageHandler;
|
||||
|
||||
- (void)asynchronouslyProcess:(santa::santad::event_providers::endpoint_security::Message)msg
|
||||
handler:
|
||||
(void (^)(santa::santad::event_providers::endpoint_security::Message &&))
|
||||
messageHandler;
|
||||
|
||||
- (void)processMessage:(santa::santad::event_providers::endpoint_security::Message &&)msg
|
||||
handler:
|
||||
(void (^)(const santa::santad::event_providers::endpoint_security::Message &))
|
||||
@@ -71,7 +90,8 @@
|
||||
|
||||
- (bool)clearCache;
|
||||
|
||||
+ (bool)isDatabasePath:(const std::string_view)path;
|
||||
+ (std::set<std::string>)getProtectedPaths;
|
||||
+ (bool)isProtectedPath:(const std::string_view)path;
|
||||
+ (bool)populateAuditTokenSelf:(audit_token_t *)tok;
|
||||
|
||||
@end
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
@@ -31,6 +32,7 @@
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::Processor;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFile;
|
||||
@@ -104,7 +106,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
es_message_t esMsg;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
@@ -122,7 +124,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_FORK, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
// Have subscribe fail the first time, meaning clear cache only called once.
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_ALLOW, true))
|
||||
@@ -244,10 +246,121 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testUnsubscribeAll {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// Test the underlying unsubscribe all impl returning both true and false
|
||||
EXPECT_CALL(*mockESApi, UnsubscribeAll)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(false));
|
||||
|
||||
XCTAssertTrue([client unsubscribeAll]);
|
||||
XCTAssertFalse([client unsubscribeAll]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testUnmuteEverything {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// Test variations of underlying unmute impls returning both true and false
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(false));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
XCTAssertTrue([client unmuteEverything]);
|
||||
XCTAssertFalse([client unmuteEverything]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testEnableTargetPathWatching {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// Test the underlying invert nute impl returning both true and false
|
||||
EXPECT_CALL(*mockESApi, InvertTargetPathMuting)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(false));
|
||||
|
||||
XCTAssertTrue([client enableTargetPathWatching]);
|
||||
XCTAssertFalse([client enableTargetPathWatching]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testMuteTargetPaths {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// Ensure all paths are attempted to be muted even if some fail.
|
||||
// Ensure if any paths fail the overall result is false.
|
||||
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, "a", WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, "b", WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(false));
|
||||
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, "c", WatchItemPathType::kPrefix))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
std::vector<std::pair<std::string, WatchItemPathType>> paths = {
|
||||
{"a", WatchItemPathType::kLiteral},
|
||||
{"b", WatchItemPathType::kLiteral},
|
||||
{"c", WatchItemPathType::kPrefix},
|
||||
};
|
||||
|
||||
XCTAssertFalse([client muteTargetPaths:paths]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testUnmuteTargetPaths {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// Ensure all paths are attempted to be unmuted even if some fail.
|
||||
// Ensure if any paths fail the overall result is false.
|
||||
EXPECT_CALL(*mockESApi, UnmuteTargetPath(testing::_, "a", WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteTargetPath(testing::_, "b", WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(false));
|
||||
EXPECT_CALL(*mockESApi, UnmuteTargetPath(testing::_, "c", WatchItemPathType::kPrefix))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
std::vector<std::pair<std::string, WatchItemPathType>> paths = {
|
||||
{"a", WatchItemPathType::kLiteral},
|
||||
{"b", WatchItemPathType::kLiteral},
|
||||
{"c", WatchItemPathType::kPrefix},
|
||||
};
|
||||
|
||||
XCTAssertFalse([client unmuteTargetPaths:paths]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testRespondToMessageWithAuthResultCacheable {
|
||||
es_message_t esMsg;
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
es_auth_result_t result = ES_AUTH_RESULT_DENY;
|
||||
bool cacheable = true;
|
||||
@@ -270,44 +383,41 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
}
|
||||
|
||||
- (void)testProcessEnrichedMessageHandler {
|
||||
es_message_t esMsg;
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
// Note: In this test, `RetainMessage` isn't setup to return anything. This
|
||||
// means that the underlying `es_msg_` in the `Message` object is NULL, and
|
||||
// therefore no call to `ReleaseMessage` is ever made (hence no expectations).
|
||||
// Because we don't need to operate on the es_msg_, this simplifies the test.
|
||||
EXPECT_CALL(*mockESApi, RetainMessage);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
{
|
||||
auto enrichedMsg = std::make_shared<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &esMsg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
es_message_t esMsg;
|
||||
auto enrichedMsg = std::make_shared<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &esMsg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
[client processEnrichedMessage:enrichedMsg
|
||||
handler:^(std::shared_ptr<EnrichedMessage> msg) {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
[client processEnrichedMessage:enrichedMsg
|
||||
handler:^(std::shared_ptr<EnrichedMessage> msg) {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Handler block not called within expected time window");
|
||||
XCTAssertEqual(
|
||||
0, dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Handler block not called within expected time window");
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testIsDatabasePath {
|
||||
XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/rules.db"]);
|
||||
XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/events.db"]);
|
||||
- (void)testIsProtectedPath {
|
||||
XCTAssertTrue([SNTEndpointSecurityClient isProtectedPath:"/private/var/db/santa/rules.db"]);
|
||||
XCTAssertTrue([SNTEndpointSecurityClient isProtectedPath:"/private/var/db/santa/events.db"]);
|
||||
|
||||
XCTAssertFalse([SNTEndpointSecurityClient isDatabasePath:"/not/a/db/path"]);
|
||||
XCTAssertFalse([SNTEndpointSecurityClient isProtectedPath:"/not/a/db/path"]);
|
||||
}
|
||||
|
||||
- (void)testProcessMessageHandlerBadEventType {
|
||||
@@ -316,7 +426,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
@@ -345,7 +455,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
45 * 1000); // Long deadline to not hit
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
@@ -359,11 +469,11 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
handler:^(const Message &msg) {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}]);
|
||||
}
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
"Handler block not called within expected time window");
|
||||
XCTAssertEqual(
|
||||
0, dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
"Handler block not called within expected time window");
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
@@ -371,7 +481,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
- (void)testProcessMessageHandlerWithDeadlineTimeout {
|
||||
// Set a es_message_t deadline of 750ms
|
||||
// Set a deadline leeway in the `SNTEndpointSecurityClient` of 500ms
|
||||
// Mock `RespondAuthResult` which is called from the deadline handler
|
||||
// Mock `RespondFlagsResult` which is called from the deadline handler
|
||||
// Signal the semaphore from the mock
|
||||
// Wait a few seconds for the semaphore (should take ~250ms)
|
||||
//
|
||||
@@ -386,12 +496,12 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
750); // 750ms timeout
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
dispatch_semaphore_t deadlineSema = dispatch_semaphore_create(0);
|
||||
dispatch_semaphore_t controlSema = dispatch_semaphore_create(0);
|
||||
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_DENY, false))
|
||||
EXPECT_CALL(*mockESApi, RespondFlagsResult(testing::_, testing::_, 0x0, false))
|
||||
.WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
// Signal deadlineSema to let the handler block continue execution
|
||||
dispatch_semaphore_signal(deadlineSema);
|
||||
|
||||
@@ -237,6 +237,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue];
|
||||
NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey];
|
||||
BOOL isUSB = [protocol isEqualToString:@"USB"];
|
||||
BOOL isSecureDigital = [protocol isEqualToString:@"Secure Digital"];
|
||||
BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"];
|
||||
|
||||
NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey];
|
||||
@@ -249,8 +250,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// If the device is internal or virtual we are okay with the operation. We
|
||||
// also are okay with operations for devices that are non-removal as long as
|
||||
// they are NOT a USB device.
|
||||
if (isInternal || isVirtual || (!isRemovable && !isEjectable && !isUSB)) {
|
||||
// they are NOT a USB device, or an SD Card.
|
||||
if (isInternal || isVirtual || (!isRemovable && !isEjectable && !isUSB && !isSecureDigital)) {
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,8 +124,6 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(eventType, &proc, ActionType::Auth, 6000);
|
||||
// Need a pointer to esMsg to capture in blocks below.
|
||||
es_message_t *heapESMsg = &esMsg;
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
@@ -142,7 +140,6 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
});
|
||||
EXPECT_CALL(*mockESApi, RetainMessage).WillRepeatedly(^{
|
||||
retainCount++;
|
||||
return heapESMsg;
|
||||
});
|
||||
|
||||
if (eventType == ES_EVENT_TYPE_AUTH_MOUNT) {
|
||||
@@ -317,7 +314,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, FlushCache);
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
@@ -32,3 +34,22 @@
|
||||
- (void)enable;
|
||||
|
||||
@end
|
||||
|
||||
// Extension of the `SNTEndpointSecurityEventHandler` protocol for
|
||||
// `SNTEndpointSecurityClient` subclasses that can be dynamically
|
||||
// enabled and disabled.
|
||||
@protocol SNTEndpointSecurityDynamicEventHandler <SNTEndpointSecurityEventHandler>
|
||||
|
||||
// Called when a client should no longer receive events.
|
||||
- (void)disable;
|
||||
|
||||
- (void)
|
||||
watchItemsCount:(size_t)count
|
||||
newPaths:
|
||||
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>>
|
||||
&)newPaths
|
||||
removedPaths:
|
||||
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>> &)
|
||||
removedPaths;
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/// Copyright 2022 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>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/santad/DataLayer/WatchItems.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizer
|
||||
: SNTEndpointSecurityClient <SNTEndpointSecurityDynamicEventHandler>
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
watchItems:(std::shared_ptr<santa::santad::data_layer::WatchItems>)watchItems
|
||||
enricher:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
|
||||
decisionCache:(SNTDecisionCache *)decisionCache;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,547 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityFileAccessAuthorizer.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#include <sys/fcntl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
|
||||
#include "Source/common/Platform.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/common/SantaVnode.h"
|
||||
#include "Source/common/SantaVnodeHash.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/DataLayer/WatchItems.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
using santa::santad::data_layer::WatchItems;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichOptions;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
NSString *kBadCertHash = @"BAD_CERT_HASH";
|
||||
|
||||
static constexpr uint32_t kOpenFlagsIndicatingWrite = FWRITE | O_APPEND | O_TRUNC;
|
||||
|
||||
// Small structure to hold a complete event path target being operated upon and
|
||||
// a bool indicating whether the path is a readable target (e.g. a file being
|
||||
// opened or cloned)
|
||||
struct PathTarget {
|
||||
std::string path;
|
||||
bool isReadable;
|
||||
};
|
||||
|
||||
static inline std::string Path(const es_file_t *esFile) {
|
||||
return std::string(esFile->path.data, esFile->path.length);
|
||||
}
|
||||
|
||||
static inline std::string Path(const es_string_token_t &tok) {
|
||||
return std::string(tok.data, tok.length);
|
||||
}
|
||||
|
||||
static inline void PushBackIfNotTruncated(std::vector<PathTarget> &vec, const es_file_t *esFile,
|
||||
bool isReadable = false) {
|
||||
if (!esFile->path_truncated) {
|
||||
vec.push_back({Path(esFile), isReadable});
|
||||
}
|
||||
}
|
||||
|
||||
static inline void PushBackIfNotTruncated(std::vector<PathTarget> &vec, const es_file_t *dir,
|
||||
const es_string_token_t &name, bool isReadable = false) {
|
||||
if (!dir->path_truncated) {
|
||||
vec.push_back({Path(dir) + "/" + Path(name), isReadable});
|
||||
}
|
||||
}
|
||||
|
||||
es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyDecision decision) {
|
||||
switch (decision) {
|
||||
case FileAccessPolicyDecision::kNoPolicy: return ES_AUTH_RESULT_ALLOW;
|
||||
case FileAccessPolicyDecision::kDenied: return ES_AUTH_RESULT_DENY;
|
||||
case FileAccessPolicyDecision::kDeniedInvalidSignature: return ES_AUTH_RESULT_DENY;
|
||||
case FileAccessPolicyDecision::kAllowed: return ES_AUTH_RESULT_ALLOW;
|
||||
case FileAccessPolicyDecision::kAllowedReadAccess: return ES_AUTH_RESULT_ALLOW;
|
||||
case FileAccessPolicyDecision::kAllowedAuditOnly: return ES_AUTH_RESULT_ALLOW;
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid file access decision encountered: %d", decision);
|
||||
[NSException raise:@"Invalid FileAccessPolicyDecision"
|
||||
format:@"Invalid FileAccessPolicyDecision: %d", decision];
|
||||
}
|
||||
}
|
||||
|
||||
bool ShouldLogDecision(FileAccessPolicyDecision decision) {
|
||||
switch (decision) {
|
||||
case FileAccessPolicyDecision::kDenied: return true;
|
||||
case FileAccessPolicyDecision::kDeniedInvalidSignature: return true;
|
||||
case FileAccessPolicyDecision::kAllowedAuditOnly: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_result_t result2) {
|
||||
// If either policy denied the operation, the operation is denied
|
||||
return ((result1 == ES_AUTH_RESULT_DENY || result2 == ES_AUTH_RESULT_DENY)
|
||||
? ES_AUTH_RESULT_DENY
|
||||
: ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
|
||||
void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
switch (msg->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_CLONE:
|
||||
PushBackIfNotTruncated(targets, msg->event.clone.source, true);
|
||||
PushBackIfNotTruncated(targets, msg->event.clone.target_dir, msg->event.clone.target_name);
|
||||
break;
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_CREATE:
|
||||
// AUTH CREATE events should always be ES_DESTINATION_TYPE_NEW_PATH
|
||||
if (msg->event.create.destination_type == ES_DESTINATION_TYPE_NEW_PATH) {
|
||||
PushBackIfNotTruncated(targets, msg->event.create.destination.new_path.dir,
|
||||
msg->event.create.destination.new_path.filename);
|
||||
} else {
|
||||
LOGW(@"Unexpected destination type for create event: %d. Ignoring target.",
|
||||
msg->event.create.destination_type);
|
||||
}
|
||||
break;
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_COPYFILE:
|
||||
PushBackIfNotTruncated(targets, msg->event.copyfile.source, true);
|
||||
if (msg->event.copyfile.target_file) {
|
||||
PushBackIfNotTruncated(targets, msg->event.copyfile.target_file);
|
||||
} else {
|
||||
PushBackIfNotTruncated(targets, msg->event.copyfile.target_dir,
|
||||
msg->event.copyfile.target_name);
|
||||
}
|
||||
break;
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_EXCHANGEDATA:
|
||||
PushBackIfNotTruncated(targets, msg->event.exchangedata.file1);
|
||||
PushBackIfNotTruncated(targets, msg->event.exchangedata.file2);
|
||||
break;
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_LINK:
|
||||
PushBackIfNotTruncated(targets, msg->event.link.source);
|
||||
PushBackIfNotTruncated(targets, msg->event.link.target_dir, msg->event.link.target_filename);
|
||||
break;
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_OPEN:
|
||||
PushBackIfNotTruncated(targets, msg->event.open.file, true);
|
||||
break;
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_RENAME:
|
||||
PushBackIfNotTruncated(targets, msg->event.rename.source);
|
||||
if (msg->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
|
||||
PushBackIfNotTruncated(targets, msg->event.rename.destination.existing_file);
|
||||
} else if (msg->event.rename.destination_type == ES_DESTINATION_TYPE_NEW_PATH) {
|
||||
PushBackIfNotTruncated(targets, msg->event.rename.destination.new_path.dir,
|
||||
msg->event.rename.destination.new_path.filename);
|
||||
} else {
|
||||
LOGW(@"Unexpected destination type for rename event: %d. Ignoring destination.",
|
||||
msg->event.rename.destination_type);
|
||||
}
|
||||
break;
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_TRUNCATE:
|
||||
PushBackIfNotTruncated(targets, msg->event.truncate.target);
|
||||
break;
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_UNLINK:
|
||||
PushBackIfNotTruncated(targets, msg->event.unlink.target);
|
||||
break;
|
||||
|
||||
default:
|
||||
[NSException
|
||||
raise:@"Unexpected event type"
|
||||
format:@"File Access Authorizer client does not handle event: %d", msg->event_type];
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizer ()
|
||||
@property SNTDecisionCache *decisionCache;
|
||||
@property bool isSubscribed;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityFileAccessAuthorizer {
|
||||
std::shared_ptr<Logger> _logger;
|
||||
std::shared_ptr<WatchItems> _watchItems;
|
||||
std::shared_ptr<Enricher> _enricher;
|
||||
SantaCache<SantaVnode, NSString *> _certHashCache;
|
||||
}
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
watchItems:(std::shared_ptr<WatchItems>)watchItems
|
||||
enricher:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
|
||||
decisionCache:(SNTDecisionCache *)decisionCache {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:std::move(metrics)
|
||||
processor:santa::santad::Processor::kFileAccessAuthorizer];
|
||||
if (self) {
|
||||
_watchItems = std::move(watchItems);
|
||||
_logger = std::move(logger);
|
||||
_enricher = std::move(enricher);
|
||||
|
||||
_decisionCache = decisionCache;
|
||||
|
||||
[self establishClientOrDie];
|
||||
|
||||
[super enableTargetPathWatching];
|
||||
[super unmuteEverything];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return @"FileAccessAuthorizer";
|
||||
}
|
||||
|
||||
- (NSString *)getCertificateHash:(es_file_t *)esFile {
|
||||
// First see if we've already cached this value
|
||||
SantaVnode vnodeID = SantaVnode::VnodeForFile(esFile);
|
||||
NSString *result = self->_certHashCache.get(vnodeID);
|
||||
if (!result) {
|
||||
// If this wasn't already cached, try finding a cached SNTCachedDecision
|
||||
SNTCachedDecision *cd = [self.decisionCache cachedDecisionForFile:esFile->stat];
|
||||
if (cd) {
|
||||
// There was an existing cached decision, use its cert hash
|
||||
result = cd.certSHA256;
|
||||
} else {
|
||||
// If the cached decision didn't exist, try a manual lookup
|
||||
NSError *e;
|
||||
MOLCodesignChecker *csInfo =
|
||||
[[MOLCodesignChecker alloc] initWithBinaryPath:@(esFile->path.data) error:&e];
|
||||
if (!e) {
|
||||
result = csInfo.leafCertificate.SHA256;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.length) {
|
||||
// If result is still nil, there isn't much recourse... We will
|
||||
// assume that this error isn't transient and set a terminal value
|
||||
// in the cache to prevent continous attempts to lookup cert hash.
|
||||
result = kBadCertHash;
|
||||
}
|
||||
|
||||
// Finally, add the result to the cache to prevent future lookups
|
||||
self->_certHashCache.set(vnodeID, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (FileAccessPolicyDecision)specialCaseForPolicy:(std::shared_ptr<WatchItemPolicy>)policy
|
||||
target:(const PathTarget &)target
|
||||
message:(const Message &)msg {
|
||||
switch (msg->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_OPEN:
|
||||
// If the policy is write-only, but the operation isn't a write action, it's allowed
|
||||
if (policy->allow_read_access && !(msg->event.open.fflag & kOpenFlagsIndicatingWrite)) {
|
||||
return FileAccessPolicyDecision::kAllowedReadAccess;
|
||||
}
|
||||
break;
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_CLONE:
|
||||
// If policy is write-only, readable targets are allowed (e.g. source file)
|
||||
if (policy->allow_read_access && target.isReadable) {
|
||||
return FileAccessPolicyDecision::kAllowedReadAccess;
|
||||
}
|
||||
break;
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_COPYFILE:
|
||||
// Note: Flags for the copyfile event represent the kernel view, not the usersapce
|
||||
// copyfile(3) implementation. This means if a `copyfile(3)` flag like `COPYFILE_MOVE`
|
||||
// is specified, it will come as a separate `unlink(2)` event, not a flag here.
|
||||
if (policy->allow_read_access && target.isReadable) {
|
||||
return FileAccessPolicyDecision::kAllowedReadAccess;
|
||||
}
|
||||
break;
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_CREATE:
|
||||
case ES_EVENT_TYPE_AUTH_EXCHANGEDATA:
|
||||
case ES_EVENT_TYPE_AUTH_LINK:
|
||||
case ES_EVENT_TYPE_AUTH_RENAME:
|
||||
case ES_EVENT_TYPE_AUTH_TRUNCATE:
|
||||
case ES_EVENT_TYPE_AUTH_UNLINK:
|
||||
// These event types have no special case
|
||||
break;
|
||||
|
||||
default:
|
||||
[NSException raise:@"Unexpected event type"
|
||||
format:@"Received unexpected event type in the file access authorizer: %d",
|
||||
msg->event_type];
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
return FileAccessPolicyDecision::kNoPolicy;
|
||||
}
|
||||
|
||||
/// An An `es_process_t` must match all criteria within the given
|
||||
/// WatchItemPolicy::Process to be considered a match.
|
||||
- (bool)policyProcess:(const WatchItemPolicy::Process &)policyProc
|
||||
matchesESProcess:(const es_process_t *)esProc {
|
||||
// Note: Intentionally not checking `CS_VALID` here - this check must happen
|
||||
// outside of this method. This method is used to individually check each
|
||||
// configured process exception while the check for a valid code signature
|
||||
// is more broad and applies whether or not process exceptions exist.
|
||||
if (esProc->codesigning_flags & CS_SIGNED) {
|
||||
// Check if the instigating process has an allowed TeamID
|
||||
if (!policyProc.team_id.empty() && esProc->team_id.data &&
|
||||
policyProc.team_id != esProc->team_id.data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!policyProc.signing_id.empty() && esProc->signing_id.data &&
|
||||
policyProc.signing_id != esProc->signing_id.data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the instigating process has an allowed CDHash
|
||||
if (policyProc.cdhash.size() == CS_CDHASH_LEN &&
|
||||
std::memcmp(policyProc.cdhash.data(), esProc->cdhash, CS_CDHASH_LEN) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the instigating process has an allowed certificate hash
|
||||
if (!policyProc.certificate_sha256.empty()) {
|
||||
NSString *result = [self getCertificateHash:esProc->executable];
|
||||
if (!result || policyProc.certificate_sha256 != [result UTF8String]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the process isn't signed, ensure the policy doesn't contain any
|
||||
// attributes that require a signature
|
||||
if (!policyProc.team_id.empty() || !policyProc.signing_id.empty() ||
|
||||
policyProc.cdhash.size() == CS_CDHASH_LEN || !policyProc.certificate_sha256.empty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the instigating process path opening the file is allowed
|
||||
if (policyProc.binary_path.length() > 0 &&
|
||||
policyProc.binary_path != esProc->executable->path.data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// The operation is allowed when:
|
||||
// - No policy exists
|
||||
// - The policy is write-only, but the operation is read-only
|
||||
// - The operation was instigated by an allowed process
|
||||
// - If the instigating process is signed, the codesignature is valid
|
||||
// Otherwise the operation is denied.
|
||||
- (FileAccessPolicyDecision)applyPolicy:
|
||||
(std::optional<std::shared_ptr<WatchItemPolicy>>)optionalPolicy
|
||||
forTarget:(const PathTarget &)target
|
||||
toMessage:(const Message &)msg {
|
||||
// If no policy exists, everything is allowed
|
||||
if (!optionalPolicy.has_value()) {
|
||||
return FileAccessPolicyDecision::kNoPolicy;
|
||||
}
|
||||
|
||||
// If the process is signed but has an invalid signature, it is denied
|
||||
if (((msg->process->codesigning_flags & (CS_SIGNED | CS_VALID)) == CS_SIGNED) &&
|
||||
[[SNTConfigurator configurator] enableBadSignatureProtection]) {
|
||||
// TODO(mlw): Think about how to make stronger guarantees here to handle
|
||||
// programs becoming invalid after first being granted access. Maybe we
|
||||
// should only allow things that have hardened runtime flags set?
|
||||
return FileAccessPolicyDecision::kDeniedInvalidSignature;
|
||||
}
|
||||
|
||||
std::shared_ptr<WatchItemPolicy> policy = optionalPolicy.value();
|
||||
|
||||
// Check if this action contains any special case that would produce
|
||||
// an immediate result.
|
||||
FileAccessPolicyDecision specialCase = [self specialCaseForPolicy:policy
|
||||
target:target
|
||||
message:msg];
|
||||
if (specialCase != FileAccessPolicyDecision::kNoPolicy) {
|
||||
return specialCase;
|
||||
}
|
||||
|
||||
for (const WatchItemPolicy::Process &process : policy->processes) {
|
||||
if ([self policyProcess:process matchesESProcess:msg->process]) {
|
||||
return FileAccessPolicyDecision::kAllowed;
|
||||
}
|
||||
}
|
||||
|
||||
if (policy->audit_only) {
|
||||
return FileAccessPolicyDecision::kAllowedAuditOnly;
|
||||
} else {
|
||||
// TODO(xyz): Write to TTY like in exec controller?
|
||||
// TODO(xyz): Need new config item for custom message in UI
|
||||
return FileAccessPolicyDecision::kDenied;
|
||||
}
|
||||
}
|
||||
|
||||
- (FileAccessPolicyDecision)handleMessage:(const Message &)msg
|
||||
target:(const PathTarget &)target
|
||||
policy:
|
||||
(std::optional<std::shared_ptr<WatchItemPolicy>>)optionalPolicy
|
||||
policyVersion:(const std::string &)policyVersion {
|
||||
FileAccessPolicyDecision policyDecision = [self applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:msg];
|
||||
|
||||
if (ShouldLogDecision(policyDecision)) {
|
||||
if (optionalPolicy.has_value()) {
|
||||
std::string policyNameCopy = optionalPolicy.value()->name;
|
||||
std::string policyVersionCopy = policyVersion;
|
||||
std::string targetPathCopy = target.path;
|
||||
|
||||
[self asynchronouslyProcess:msg
|
||||
handler:^(Message &&esMsg) {
|
||||
self->_logger->LogFileAccess(
|
||||
policyVersionCopy, policyNameCopy, esMsg,
|
||||
self->_enricher->Enrich(*esMsg->process, EnrichOptions::kLocalOnly),
|
||||
targetPathCopy, policyDecision);
|
||||
}];
|
||||
|
||||
} else {
|
||||
LOGE(@"Unexpectedly missing policy: Unable to log file access event: %s -> %s",
|
||||
Path(msg->process->executable).data(), target.path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
return policyDecision;
|
||||
}
|
||||
|
||||
- (void)processMessage:(const Message &)msg {
|
||||
std::vector<PathTarget> targets;
|
||||
targets.reserve(2);
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
// Extract the paths from the vector of PathTargets in order to lookup policies
|
||||
// Note: There should only ever be 1 or 2 items in the vector
|
||||
std::vector<std::string_view> paths;
|
||||
paths.reserve(2);
|
||||
for (const PathTarget &target : targets) {
|
||||
paths.push_back(std::string_view(target.path));
|
||||
}
|
||||
|
||||
WatchItems::VersionAndPolicies versionAndPolicies = self->_watchItems->FindPolciesForPaths(paths);
|
||||
|
||||
es_auth_result_t policyResult = ES_AUTH_RESULT_ALLOW;
|
||||
bool allow_read_access = false;
|
||||
|
||||
for (size_t i = 0; i < targets.size(); i++) {
|
||||
FileAccessPolicyDecision curDecision = [self handleMessage:msg
|
||||
target:targets[i]
|
||||
policy:versionAndPolicies.second[i]
|
||||
policyVersion:versionAndPolicies.first];
|
||||
|
||||
policyResult =
|
||||
CombinePolicyResults(policyResult, FileAccessPolicyDecisionToESAuthResult(curDecision));
|
||||
|
||||
// If the overall policy result is deny, then reset allow_read_access.
|
||||
// Otherwise if the current decision would allow read access, set the flag.
|
||||
if (policyResult == ES_AUTH_RESULT_DENY) {
|
||||
allow_read_access = false;
|
||||
} else if (curDecision == FileAccessPolicyDecision::kAllowedReadAccess) {
|
||||
allow_read_access = true;
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: A response is only cacheable if the policy result was explicitly
|
||||
// allowed. An "allow read access" result must not be cached to ensure a future
|
||||
// non-read accesss can be evaluated. Similarly, denied results must never be
|
||||
// cached so access attempts can be logged.
|
||||
[self respondToMessage:msg
|
||||
withAuthResult:policyResult
|
||||
cacheable:(policyResult == ES_AUTH_RESULT_ALLOW && !allow_read_access)];
|
||||
}
|
||||
|
||||
- (void)handleMessage:(santa::santad::event_providers::endpoint_security::Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
|
||||
[self processMessage:std::move(esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
[self processMessage:msg];
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)enable {
|
||||
// TODO(xyz): Expand to support ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_TRUNCATE
|
||||
std::set<es_event_type_t> events = {
|
||||
ES_EVENT_TYPE_AUTH_CLONE, ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_EXCHANGEDATA,
|
||||
ES_EVENT_TYPE_AUTH_LINK, ES_EVENT_TYPE_AUTH_OPEN, ES_EVENT_TYPE_AUTH_RENAME,
|
||||
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK,
|
||||
};
|
||||
|
||||
#if HAVE_MACOS_12
|
||||
if (@available(macOS 12.0, *)) {
|
||||
events.insert(ES_EVENT_TYPE_AUTH_COPYFILE);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!self.isSubscribed) {
|
||||
self.isSubscribed = [super subscribe:events];
|
||||
[super clearCache];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)disable {
|
||||
if (self.isSubscribed) {
|
||||
if ([super unsubscribeAll]) {
|
||||
self.isSubscribed = false;
|
||||
}
|
||||
[super unmuteEverything];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)watchItemsCount:(size_t)count
|
||||
newPaths:(const std::vector<std::pair<std::string, WatchItemPathType>> &)newPaths
|
||||
removedPaths:
|
||||
(const std::vector<std::pair<std::string, WatchItemPathType>> &)removedPaths {
|
||||
if (count == 0) {
|
||||
[self disable];
|
||||
} else {
|
||||
// Stop watching removed paths
|
||||
[super unmuteTargetPaths:removedPaths];
|
||||
|
||||
// Begin watching the added paths
|
||||
[super muteTargetPaths:newPaths];
|
||||
|
||||
// begin receiving events (if not already)
|
||||
[self enable];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,811 @@
|
||||
/// Copyright 2022 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 <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/fcntl.h>
|
||||
#include <cstring>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
#include "Source/common/Platform.h"
|
||||
#include "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/DataLayer/WatchItems.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityFileAccessAuthorizer.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/MockLogger.h"
|
||||
#include "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
extern NSString *kBadCertHash;
|
||||
|
||||
// Duplicate definition for test implementation
|
||||
struct PathTarget {
|
||||
std::string path;
|
||||
bool isReadable;
|
||||
};
|
||||
|
||||
using PathTargetsPair = std::pair<std::optional<std::string>, std::optional<std::string>>;
|
||||
extern void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets);
|
||||
extern es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyDecision decision);
|
||||
extern bool ShouldLogDecision(FileAccessPolicyDecision decision);
|
||||
extern es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_result_t result2);
|
||||
|
||||
void SetExpectationsForFileAccessAuthorizerInit(
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi) {
|
||||
EXPECT_CALL(*mockESApi, InvertTargetPathMuting).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
|
||||
}
|
||||
|
||||
// Helper to reset a policy to an empty state
|
||||
void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
proc.binary_path = "";
|
||||
proc.signing_id = "";
|
||||
proc.team_id = "";
|
||||
proc.certificate_sha256 = "";
|
||||
proc.cdhash.clear();
|
||||
}
|
||||
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizer (Testing)
|
||||
- (NSString *)getCertificateHash:(es_file_t *)esFile;
|
||||
- (FileAccessPolicyDecision)specialCaseForPolicy:(std::shared_ptr<WatchItemPolicy>)policy
|
||||
target:(const PathTarget &)target
|
||||
message:(const Message &)msg;
|
||||
- (bool)policyProcess:(const WatchItemPolicy::Process &)policyProc
|
||||
matchesESProcess:(const es_process_t *)esProc;
|
||||
- (FileAccessPolicyDecision)applyPolicy:
|
||||
(std::optional<std::shared_ptr<WatchItemPolicy>>)optionalPolicy
|
||||
forTarget:(const PathTarget &)target
|
||||
toMessage:(const Message &)msg;
|
||||
|
||||
@property bool isSubscribed;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizerTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property id cscMock;
|
||||
@property id dcMock;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityFileAccessAuthorizerTest
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
|
||||
self.cscMock = OCMClassMock([MOLCodesignChecker class]);
|
||||
OCMStub([self.cscMock alloc]).andReturn(self.cscMock);
|
||||
|
||||
self.dcMock = OCMStrictClassMock([SNTDecisionCache class]);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.cscMock stopMocking];
|
||||
[self.dcMock stopMocking];
|
||||
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void)testGetCertificateHash {
|
||||
es_file_t esFile1 = MakeESFile("foo", MakeStat(100));
|
||||
es_file_t esFile2 = MakeESFile("foo", MakeStat(200));
|
||||
es_file_t esFile3 = MakeESFile("foo", MakeStat(300));
|
||||
NSString *certHash2 = @"abc123";
|
||||
NSString *certHash3 = @"xyz789";
|
||||
NSString *got;
|
||||
NSString *want;
|
||||
id certMock = OCMClassMock([MOLCertificate class]);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
SetExpectationsForFileAccessAuthorizerInit(mockESApi);
|
||||
|
||||
SNTEndpointSecurityFileAccessAuthorizer *accessClient =
|
||||
[[SNTEndpointSecurityFileAccessAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr
|
||||
watchItems:nullptr
|
||||
enricher:nullptr
|
||||
decisionCache:self.dcMock];
|
||||
|
||||
//
|
||||
// Test 1 - Not in local cache or decision cache, and code sig lookup fails
|
||||
//
|
||||
OCMExpect([self.dcMock cachedDecisionForFile:esFile1.stat])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(nil);
|
||||
|
||||
NSError *err = [NSError errorWithDomain:@"" code:errSecCSSignatureFailed userInfo:nil];
|
||||
OCMExpect([self.cscMock initWithBinaryPath:OCMOCK_ANY error:[OCMArg setTo:err]])
|
||||
.andReturn(self.cscMock);
|
||||
|
||||
got = [accessClient getCertificateHash:&esFile1];
|
||||
want = kBadCertHash;
|
||||
|
||||
XCTAssertEqualObjects(got, want);
|
||||
|
||||
// Call again without setting new expectations on dcMock to ensure the
|
||||
// cached value is used
|
||||
got = [accessClient getCertificateHash:&esFile1];
|
||||
XCTAssertEqualObjects(got, want);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(self.dcMock));
|
||||
|
||||
//
|
||||
// Test 2 - Not in local cache or decision cache, code sig lookup successful
|
||||
//
|
||||
OCMExpect([self.dcMock cachedDecisionForFile:esFile2.stat])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(nil);
|
||||
OCMExpect([self.cscMock initWithBinaryPath:OCMOCK_ANY error:[OCMArg setTo:nil]])
|
||||
.andReturn(self.cscMock);
|
||||
|
||||
OCMExpect([self.cscMock leafCertificate]).andReturn(certMock);
|
||||
OCMExpect([certMock SHA256]).andReturn(certHash2);
|
||||
|
||||
got = [accessClient getCertificateHash:&esFile2];
|
||||
want = certHash2;
|
||||
|
||||
XCTAssertEqualObjects(got, want);
|
||||
|
||||
// Call again without setting new expectations on dcMock to ensure the
|
||||
// cached value is used
|
||||
got = [accessClient getCertificateHash:&esFile2];
|
||||
XCTAssertEqualObjects(got, want);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(self.dcMock));
|
||||
|
||||
//
|
||||
// Test 3 - Not in local cache, but is in decision cache
|
||||
//
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.certSHA256 = certHash3;
|
||||
OCMExpect([self.dcMock cachedDecisionForFile:esFile3.stat]).ignoringNonObjectArgs().andReturn(cd);
|
||||
|
||||
got = [accessClient getCertificateHash:&esFile3];
|
||||
want = certHash3;
|
||||
|
||||
XCTAssertEqualObjects(got, want);
|
||||
|
||||
// Call again without setting new expectations on dcMock to ensure the
|
||||
// cached value is used
|
||||
got = [accessClient getCertificateHash:&esFile3];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
[certMock stopMocking];
|
||||
}
|
||||
|
||||
- (void)testFileAccessPolicyDecisionToESAuthResult {
|
||||
std::map<FileAccessPolicyDecision, es_auth_result_t> policyDecisionToAuthResult = {
|
||||
{FileAccessPolicyDecision::kNoPolicy, ES_AUTH_RESULT_ALLOW},
|
||||
{FileAccessPolicyDecision::kDenied, ES_AUTH_RESULT_DENY},
|
||||
{FileAccessPolicyDecision::kDeniedInvalidSignature, ES_AUTH_RESULT_DENY},
|
||||
{FileAccessPolicyDecision::kAllowed, ES_AUTH_RESULT_ALLOW},
|
||||
{FileAccessPolicyDecision::kAllowedReadAccess, ES_AUTH_RESULT_ALLOW},
|
||||
{FileAccessPolicyDecision::kAllowedAuditOnly, ES_AUTH_RESULT_ALLOW},
|
||||
};
|
||||
|
||||
for (const auto &kv : policyDecisionToAuthResult) {
|
||||
XCTAssertEqual(FileAccessPolicyDecisionToESAuthResult(kv.first), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(FileAccessPolicyDecisionToESAuthResult((FileAccessPolicyDecision)123));
|
||||
}
|
||||
|
||||
- (void)testShouldLogDecision {
|
||||
std::map<FileAccessPolicyDecision, bool> policyDecisionToShouldLog = {
|
||||
{FileAccessPolicyDecision::kNoPolicy, false},
|
||||
{FileAccessPolicyDecision::kDenied, true},
|
||||
{FileAccessPolicyDecision::kDeniedInvalidSignature, true},
|
||||
{FileAccessPolicyDecision::kAllowed, false},
|
||||
{FileAccessPolicyDecision::kAllowedReadAccess, false},
|
||||
{FileAccessPolicyDecision::kAllowedAuditOnly, true},
|
||||
{(FileAccessPolicyDecision)5, false},
|
||||
};
|
||||
|
||||
for (const auto &kv : policyDecisionToShouldLog) {
|
||||
XCTAssertEqual(ShouldLogDecision(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testCombinePolicyResults {
|
||||
// Ensure that the combined result is ES_AUTH_RESULT_DENY if both or either
|
||||
// input result is ES_AUTH_RESULT_DENY.
|
||||
XCTAssertEqual(CombinePolicyResults(ES_AUTH_RESULT_DENY, ES_AUTH_RESULT_DENY),
|
||||
ES_AUTH_RESULT_DENY);
|
||||
|
||||
XCTAssertEqual(CombinePolicyResults(ES_AUTH_RESULT_DENY, ES_AUTH_RESULT_ALLOW),
|
||||
ES_AUTH_RESULT_DENY);
|
||||
|
||||
XCTAssertEqual(CombinePolicyResults(ES_AUTH_RESULT_ALLOW, ES_AUTH_RESULT_DENY),
|
||||
ES_AUTH_RESULT_DENY);
|
||||
|
||||
XCTAssertEqual(CombinePolicyResults(ES_AUTH_RESULT_ALLOW, ES_AUTH_RESULT_ALLOW),
|
||||
ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
|
||||
- (void)testSpecialCaseForPolicyMessage {
|
||||
es_file_t esFile = MakeESFile("foo");
|
||||
es_process_t esProc = MakeESProcess(&esFile);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &esProc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
SetExpectationsForFileAccessAuthorizerInit(mockESApi);
|
||||
|
||||
SNTEndpointSecurityFileAccessAuthorizer *accessClient =
|
||||
[[SNTEndpointSecurityFileAccessAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr
|
||||
watchItems:nullptr
|
||||
enricher:nullptr
|
||||
decisionCache:nil];
|
||||
|
||||
auto policy = std::make_shared<WatchItemPolicy>("foo_policy", "/foo");
|
||||
|
||||
FileAccessPolicyDecision result;
|
||||
PathTarget target = {.path = "/some/random/path", .isReadable = true};
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_OPEN;
|
||||
|
||||
// Write-only policy, Write operation
|
||||
{
|
||||
policy->allow_read_access = true;
|
||||
esMsg.event.open.fflag = FWRITE | FREAD;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
result = [accessClient specialCaseForPolicy:policy target:target message:msg];
|
||||
XCTAssertEqual(result, FileAccessPolicyDecision::kNoPolicy);
|
||||
}
|
||||
|
||||
// Write-only policy, Read operation
|
||||
{
|
||||
policy->allow_read_access = true;
|
||||
esMsg.event.open.fflag = FREAD;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
result = [accessClient specialCaseForPolicy:policy target:target message:msg];
|
||||
XCTAssertEqual(result, FileAccessPolicyDecision::kAllowedReadAccess);
|
||||
}
|
||||
|
||||
// Read/Write policy, Read operation
|
||||
{
|
||||
policy->allow_read_access = false;
|
||||
esMsg.event.open.fflag = FREAD;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
result = [accessClient specialCaseForPolicy:policy target:target message:msg];
|
||||
XCTAssertEqual(result, FileAccessPolicyDecision::kNoPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_CLONE;
|
||||
|
||||
// Write-only policy, target readable
|
||||
{
|
||||
policy->allow_read_access = true;
|
||||
target.isReadable = true;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
result = [accessClient specialCaseForPolicy:policy target:target message:msg];
|
||||
XCTAssertEqual(result, FileAccessPolicyDecision::kAllowedReadAccess);
|
||||
}
|
||||
|
||||
// Write-only policy, target not readable
|
||||
{
|
||||
policy->allow_read_access = true;
|
||||
target.isReadable = false;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
result = [accessClient specialCaseForPolicy:policy target:target message:msg];
|
||||
XCTAssertEqual(result, FileAccessPolicyDecision::kNoPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_COPYFILE;
|
||||
|
||||
// Write-only policy, target readable
|
||||
{
|
||||
policy->allow_read_access = true;
|
||||
target.isReadable = true;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
result = [accessClient specialCaseForPolicy:policy target:target message:msg];
|
||||
XCTAssertEqual(result, FileAccessPolicyDecision::kAllowedReadAccess);
|
||||
}
|
||||
|
||||
// Write-only policy, target not readable
|
||||
{
|
||||
policy->allow_read_access = true;
|
||||
target.isReadable = false;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
result = [accessClient specialCaseForPolicy:policy target:target message:msg];
|
||||
XCTAssertEqual(result, FileAccessPolicyDecision::kNoPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure other handled event types do not have a special case
|
||||
std::set<es_event_type_t> eventTypes = {
|
||||
ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_EXCHANGEDATA, ES_EVENT_TYPE_AUTH_LINK,
|
||||
ES_EVENT_TYPE_AUTH_RENAME, ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK,
|
||||
};
|
||||
|
||||
for (const auto &event : eventTypes) {
|
||||
esMsg.event_type = event;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
result = [accessClient specialCaseForPolicy:policy target:target message:msg];
|
||||
XCTAssertEqual(result, FileAccessPolicyDecision::kNoPolicy);
|
||||
}
|
||||
|
||||
// Ensure unsubscribed event types throw an exception
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_SIGNAL;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertThrows([accessClient specialCaseForPolicy:policy target:target message:msg]);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testPolicyProcessMatchesESProcess {
|
||||
const char *instigatingCertHash = "abc123";
|
||||
const char *teamId = "myvalidtid";
|
||||
const char *signingId = "com.google.test";
|
||||
std::vector<uint8_t> cdhashBytes(CS_CDHASH_LEN);
|
||||
std::fill(cdhashBytes.begin(), cdhashBytes.end(), 0xAA);
|
||||
es_file_t esFile = MakeESFile("foo");
|
||||
es_process_t esProc = MakeESProcess(&esFile);
|
||||
esProc.codesigning_flags = CS_SIGNED;
|
||||
esProc.team_id = MakeESStringToken(teamId);
|
||||
esProc.signing_id = MakeESStringToken(signingId);
|
||||
std::memcpy(esProc.cdhash, cdhashBytes.data(), sizeof(esProc.cdhash));
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
SetExpectationsForFileAccessAuthorizerInit(mockESApi);
|
||||
|
||||
SNTEndpointSecurityFileAccessAuthorizer *accessClient =
|
||||
[[SNTEndpointSecurityFileAccessAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr
|
||||
watchItems:nullptr
|
||||
enricher:nullptr
|
||||
decisionCache:nil];
|
||||
|
||||
id accessClientMock = OCMPartialMock(accessClient);
|
||||
|
||||
OCMStub([accessClientMock getCertificateHash:&esFile])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(@(instigatingCertHash));
|
||||
|
||||
WatchItemPolicy::Process policyProc("", "", "", {}, "");
|
||||
|
||||
{
|
||||
// Process policy matching single attribute - path
|
||||
ClearWatchItemPolicyProcess(policyProc);
|
||||
policyProc.binary_path = "foo";
|
||||
XCTAssertTrue([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
policyProc.binary_path = "badpath";
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
}
|
||||
|
||||
{
|
||||
// Process policy matching single attribute - SigningID
|
||||
ClearWatchItemPolicyProcess(policyProc);
|
||||
policyProc.signing_id = signingId;
|
||||
XCTAssertTrue([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
policyProc.signing_id = "badid";
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
}
|
||||
|
||||
{
|
||||
// Process policy matching single attribute - TeamID
|
||||
ClearWatchItemPolicyProcess(policyProc);
|
||||
policyProc.team_id = teamId;
|
||||
XCTAssertTrue([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
policyProc.team_id = "badid";
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
}
|
||||
|
||||
{
|
||||
// Process policy matching single attribute - cert hash
|
||||
ClearWatchItemPolicyProcess(policyProc);
|
||||
policyProc.certificate_sha256 = instigatingCertHash;
|
||||
XCTAssertTrue([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
policyProc.certificate_sha256 = "badcert";
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
}
|
||||
|
||||
{
|
||||
// Process policy matching single attribute - cdhash
|
||||
ClearWatchItemPolicyProcess(policyProc);
|
||||
policyProc.cdhash = cdhashBytes;
|
||||
XCTAssertTrue([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
policyProc.cdhash[0] = 0x0;
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
}
|
||||
|
||||
{
|
||||
// Process policy with only a subset of matching attributes
|
||||
ClearWatchItemPolicyProcess(policyProc);
|
||||
policyProc.binary_path = "foo";
|
||||
policyProc.team_id = "invalidtid";
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
}
|
||||
|
||||
{
|
||||
// Process policy with codesigning-based attributes, but unsigned ES process
|
||||
ClearWatchItemPolicyProcess(policyProc);
|
||||
esProc.codesigning_flags = 0x0;
|
||||
policyProc.team_id = "myvalidtid";
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
}
|
||||
|
||||
[accessClientMock stopMocking];
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testApplyPolicyToMessage {
|
||||
const char *instigatingPath = "/path/to/proc";
|
||||
const char *instigatingTeamID = "my_teamid";
|
||||
const char *instigatingCertHash = "abc123";
|
||||
WatchItemPolicy::Process policyProc(instigatingPath, "", "", {}, "");
|
||||
std::array<uint8_t, 20> instigatingCDHash;
|
||||
instigatingCDHash.fill(0x41);
|
||||
es_file_t esFile = MakeESFile(instigatingPath);
|
||||
es_process_t esProc = MakeESProcess(&esFile);
|
||||
esProc.team_id = MakeESStringToken(instigatingTeamID);
|
||||
memcpy(esProc.cdhash, instigatingCDHash.data(), sizeof(esProc.cdhash));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &esProc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
SetExpectationsForFileAccessAuthorizerInit(mockESApi);
|
||||
|
||||
SNTEndpointSecurityFileAccessAuthorizer *accessClient =
|
||||
[[SNTEndpointSecurityFileAccessAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr
|
||||
watchItems:nullptr
|
||||
enricher:nullptr
|
||||
decisionCache:nil];
|
||||
|
||||
id accessClientMock = OCMPartialMock(accessClient);
|
||||
|
||||
PathTarget target = {.path = "/some/random/path", .isReadable = true};
|
||||
int fake;
|
||||
OCMStub([accessClientMock specialCaseForPolicy:nullptr target:target message:*(Message *)&fake])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(FileAccessPolicyDecision::kNoPolicy);
|
||||
|
||||
OCMStub([accessClientMock getCertificateHash:&esFile])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(@(instigatingCertHash));
|
||||
|
||||
// If no policy exists, the operation is allowed
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:std::nullopt forTarget:target toMessage:msg],
|
||||
FileAccessPolicyDecision::kNoPolicy);
|
||||
}
|
||||
|
||||
auto policy = std::make_shared<WatchItemPolicy>("foo_policy", "/foo");
|
||||
policy->processes.push_back(policyProc);
|
||||
auto optionalPolicy = std::make_optional<std::shared_ptr<WatchItemPolicy>>(policy);
|
||||
|
||||
// Signed but invalid instigating processes are automatically
|
||||
// denied when `EnableBadSignatureProtection` is true
|
||||
{
|
||||
OCMExpect([self.mockConfigurator enableBadSignatureProtection]).andReturn(YES);
|
||||
esMsg.process->codesigning_flags = CS_SIGNED;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
FileAccessPolicyDecision::kDeniedInvalidSignature);
|
||||
}
|
||||
|
||||
// Signed but invalid instigating processes are not automatically
|
||||
// denied when `EnableBadSignatureProtection` is false. Policy
|
||||
// evaluation should continue normally.
|
||||
{
|
||||
OCMExpect([self.mockConfigurator enableBadSignatureProtection]).andReturn(NO);
|
||||
esMsg.process->codesigning_flags = CS_SIGNED;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(true);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
FileAccessPolicyDecision::kAllowed);
|
||||
}
|
||||
|
||||
// Set the codesign flags to be signed and valid for the remaining tests
|
||||
esMsg.process->codesigning_flags = CS_SIGNED | CS_VALID;
|
||||
|
||||
// If no exceptions, operations are logged and denied
|
||||
{
|
||||
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(false);
|
||||
policy->audit_only = false;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
FileAccessPolicyDecision::kDenied);
|
||||
}
|
||||
|
||||
// For audit only policies with no exceptions, operations are logged but allowed
|
||||
{
|
||||
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(false);
|
||||
policy->audit_only = true;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testEnable {
|
||||
std::set<es_event_type_t> expectedEventSubs = {
|
||||
ES_EVENT_TYPE_AUTH_CLONE, ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_EXCHANGEDATA,
|
||||
ES_EVENT_TYPE_AUTH_LINK, ES_EVENT_TYPE_AUTH_OPEN, ES_EVENT_TYPE_AUTH_RENAME,
|
||||
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK,
|
||||
};
|
||||
|
||||
#if HAVE_MACOS_12
|
||||
if (@available(macOS 12.0, *)) {
|
||||
expectedEventSubs.insert(ES_EVENT_TYPE_AUTH_COPYFILE);
|
||||
}
|
||||
#endif
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
EXPECT_CALL(*mockESApi, ClearCache)
|
||||
.After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs))
|
||||
.WillOnce(testing::Return(true)))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
id fileAccessClient = [[SNTEndpointSecurityFileAccessAuthorizer alloc]
|
||||
initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:santa::santad::Processor::kFileAccessAuthorizer];
|
||||
|
||||
[fileAccessClient enable];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testDisable {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
SetExpectationsForFileAccessAuthorizerInit(mockESApi);
|
||||
|
||||
SNTEndpointSecurityFileAccessAuthorizer *accessClient =
|
||||
[[SNTEndpointSecurityFileAccessAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr
|
||||
watchItems:nullptr
|
||||
enricher:nullptr
|
||||
decisionCache:nil];
|
||||
|
||||
EXPECT_CALL(*mockESApi, UnsubscribeAll);
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
|
||||
|
||||
accessClient.isSubscribed = true;
|
||||
[accessClient disable];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testGetPathTargets {
|
||||
// This test ensures that the `GetPathTargets` functions returns the
|
||||
// expected combination of targets for each handled event variant
|
||||
es_file_t testFile1 = MakeESFile("test_file_1");
|
||||
es_file_t testFile2 = MakeESFile("test_file_2");
|
||||
es_file_t testDir = MakeESFile("test_dir");
|
||||
es_string_token_t testTok = MakeESStringToken("test_tok");
|
||||
std::string dirTok = std::string(testDir.path.data) + "/" + std::string(testTok.data);
|
||||
|
||||
es_message_t esMsg;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_OPEN;
|
||||
esMsg.event.open.file = &testFile1;
|
||||
|
||||
std::vector<PathTarget> targets;
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
XCTAssertEqual(targets.size(), 1);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertTrue(targets[0].isReadable);
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_LINK;
|
||||
esMsg.event.link.source = &testFile1;
|
||||
esMsg.event.link.target_dir = &testDir;
|
||||
esMsg.event.link.target_filename = testTok;
|
||||
|
||||
std::vector<PathTarget> targets;
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
XCTAssertCppStringEqual(targets[1].path, dirTok);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_RENAME;
|
||||
esMsg.event.rename.source = &testFile1;
|
||||
|
||||
{
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
esMsg.event.rename.destination.existing_file = &testFile2;
|
||||
|
||||
std::vector<PathTarget> targets;
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
esMsg.event.rename.destination.new_path.dir = &testDir;
|
||||
esMsg.event.rename.destination.new_path.filename = testTok;
|
||||
|
||||
std::vector<PathTarget> targets;
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
XCTAssertCppStringEqual(targets[1].path, dirTok);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_UNLINK;
|
||||
esMsg.event.unlink.target = &testFile1;
|
||||
|
||||
std::vector<PathTarget> targets;
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
XCTAssertEqual(targets.size(), 1);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_CLONE;
|
||||
esMsg.event.clone.source = &testFile1;
|
||||
esMsg.event.clone.target_dir = &testDir;
|
||||
esMsg.event.clone.target_name = testTok;
|
||||
|
||||
std::vector<PathTarget> targets;
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertTrue(targets[0].isReadable);
|
||||
XCTAssertCppStringEqual(targets[1].path, dirTok);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_EXCHANGEDATA;
|
||||
esMsg.event.exchangedata.file1 = &testFile1;
|
||||
esMsg.event.exchangedata.file2 = &testFile2;
|
||||
|
||||
std::vector<PathTarget> targets;
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_CREATE;
|
||||
esMsg.event.create.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
esMsg.event.create.destination.new_path.dir = &testDir;
|
||||
esMsg.event.create.destination.new_path.filename = testTok;
|
||||
|
||||
std::vector<PathTarget> targets;
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
XCTAssertEqual(targets.size(), 1);
|
||||
XCTAssertCppStringEqual(targets[0].path, dirTok);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_TRUNCATE;
|
||||
esMsg.event.truncate.target = &testFile1;
|
||||
|
||||
std::vector<PathTarget> targets;
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
XCTAssertEqual(targets.size(), 1);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
}
|
||||
|
||||
if (@available(macOS 12.0, *)) {
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_COPYFILE;
|
||||
esMsg.event.copyfile.source = &testFile1;
|
||||
esMsg.event.copyfile.target_dir = &testDir;
|
||||
esMsg.event.copyfile.target_name = testTok;
|
||||
|
||||
{
|
||||
esMsg.event.copyfile.target_file = nullptr;
|
||||
|
||||
std::vector<PathTarget> targets;
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertTrue(targets[0].isReadable);
|
||||
XCTAssertCppStringEqual(targets[1].path, dirTok);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event.copyfile.target_file = &testFile2;
|
||||
|
||||
std::vector<PathTarget> targets;
|
||||
PopulatePathTargets(msg, targets);
|
||||
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertTrue(targets[0].isReadable);
|
||||
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,7 +12,8 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTPrefixTree.h"
|
||||
#import "Source/common/PrefixTree.h"
|
||||
#import "Source/common/Unit.h"
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
@@ -37,6 +38,6 @@
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:
|
||||
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache
|
||||
prefixTree:(std::shared_ptr<SNTPrefixTree>)prefixTree;
|
||||
prefixTree:(std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>)prefixTree;
|
||||
|
||||
@end
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
@@ -48,7 +50,7 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
std::shared_ptr<AuthResultCache> _authResultCache;
|
||||
std::shared_ptr<Enricher> _enricher;
|
||||
std::shared_ptr<Logger> _logger;
|
||||
std::shared_ptr<SNTPrefixTree> _prefixTree;
|
||||
std::shared_ptr<PrefixTree<Unit>> _prefixTree;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
@@ -57,7 +59,7 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
enricher:(std::shared_ptr<Enricher>)enricher
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache
|
||||
prefixTree:(std::shared_ptr<SNTPrefixTree>)prefixTree {
|
||||
prefixTree:(std::shared_ptr<PrefixTree<Unit>>)prefixTree {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:std::move(metrics)
|
||||
processor:santa::santad::Processor::kRecorder];
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "Source/common/PrefixTree.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/common/Unit.h"
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
@@ -34,6 +36,8 @@
|
||||
#include "Source/santad/Metrics.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::Processor;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
@@ -94,7 +98,7 @@ class MockLogger : public Logger {
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg = std::shared_ptr<EnrichedMessage>(nullptr);
|
||||
|
||||
@@ -116,7 +120,7 @@ class MockLogger : public Logger {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}));
|
||||
|
||||
auto prefixTree = std::make_shared<SNTPrefixTree>();
|
||||
auto prefixTree = std::make_shared<PrefixTree<Unit>>();
|
||||
|
||||
id mockCC = OCMStrictClassMock([SNTCompilerController class]);
|
||||
|
||||
@@ -164,7 +168,7 @@ class MockLogger : public Logger {
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK;
|
||||
esMsg.event.link.source = &targetFile;
|
||||
prefixTree->AddPrefix(esMsg.event.link.source->path.data);
|
||||
prefixTree->InsertPrefix(esMsg.event.link.source->path.data, Unit{});
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
@@ -18,10 +18,12 @@
|
||||
#include <string.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
@@ -56,7 +58,7 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_UNLINK: {
|
||||
if ([SNTEndpointSecurityTamperResistance
|
||||
isDatabasePath:esMsg->event.unlink.target->path.data]) {
|
||||
isProtectedPath:esMsg->event.unlink.target->path.data]) {
|
||||
result = ES_AUTH_RESULT_DENY;
|
||||
LOGW(@"Preventing attempt to delete Santa databases!");
|
||||
}
|
||||
@@ -65,7 +67,7 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_RENAME: {
|
||||
if ([SNTEndpointSecurityTamperResistance
|
||||
isDatabasePath:esMsg->event.rename.source->path.data]) {
|
||||
isProtectedPath:esMsg->event.rename.source->path.data]) {
|
||||
result = ES_AUTH_RESULT_DENY;
|
||||
LOGW(@"Preventing attempt to rename Santa databases!");
|
||||
break;
|
||||
@@ -73,7 +75,7 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
|
||||
|
||||
if (esMsg->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
|
||||
if ([SNTEndpointSecurityTamperResistance
|
||||
isDatabasePath:esMsg->event.rename.destination.existing_file->path.data]) {
|
||||
isProtectedPath:esMsg->event.rename.destination.existing_file->path.data]) {
|
||||
result = ES_AUTH_RESULT_DENY;
|
||||
LOGW(@"Preventing attempt to overwrite Santa databases!");
|
||||
break;
|
||||
@@ -108,9 +110,20 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
|
||||
}
|
||||
|
||||
- (void)enable {
|
||||
// TODO(mlw): For macOS 13, use new mute and invert APIs to limit the
|
||||
// messages sent for these events to the Santa-specific directories
|
||||
// checked in the `handleMessage:` method.
|
||||
[super enableTargetPathWatching];
|
||||
[super unmuteEverything];
|
||||
|
||||
// Get the set of protected paths
|
||||
std::set<std::string> protectedPaths = [SNTEndpointSecurityTamperResistance getProtectedPaths];
|
||||
|
||||
// Iterate the set, and create a vector of literals to mute
|
||||
std::vector<std::pair<std::string, WatchItemPathType>> watchPaths;
|
||||
for (const auto &path : protectedPaths) {
|
||||
watchPaths.push_back({path, WatchItemPathType::kLiteral});
|
||||
}
|
||||
|
||||
// Begin watching the protected set
|
||||
[super muteTargetPaths:watchPaths];
|
||||
|
||||
[super subscribeAndClearCache:{
|
||||
ES_EVENT_TYPE_AUTH_KEXTLOAD,
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <set>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
@@ -31,6 +32,7 @@
|
||||
#import "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
@@ -61,6 +63,15 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
|
||||
.WillOnce(testing::Return(true)))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
// Setup mocks to handle inverting target path muting
|
||||
EXPECT_CALL(*mockESApi, InvertTargetPathMuting).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
|
||||
|
||||
// Setup mocks to handle muting the rules db and events db
|
||||
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, testing::_, WatchItemPathType::kLiteral))
|
||||
.WillRepeatedly(testing::Return(true));
|
||||
|
||||
SNTEndpointSecurityTamperResistance *tamperClient =
|
||||
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
@@ -100,7 +111,7 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
SNTEndpointSecurityTamperResistance *tamperClient =
|
||||
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi
|
||||
|
||||
@@ -59,6 +59,12 @@ class Logger {
|
||||
void LogDiskAppeared(NSDictionary *props);
|
||||
void LogDiskDisappeared(NSDictionary *props);
|
||||
|
||||
virtual void LogFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedProcess &enriched_process,
|
||||
const std::string &target, FileAccessPolicyDecision decision);
|
||||
|
||||
friend class santa::santad::logs::endpoint_security::LoggerPeer;
|
||||
|
||||
private:
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::BasicString;
|
||||
using santa::santad::logs::endpoint_security::serializers::Empty;
|
||||
@@ -97,4 +98,13 @@ void Logger::LogDiskDisappeared(NSDictionary *props) {
|
||||
writer_->Write(serializer_->SerializeDiskDisappeared(props));
|
||||
}
|
||||
|
||||
void Logger::LogFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedProcess &enriched_process,
|
||||
const std::string &target, FileAccessPolicyDecision decision) {
|
||||
writer_->Write(serializer_->SerializeFileAccess(policy_version, policy_name, msg,
|
||||
enriched_process, target, decision));
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security
|
||||
|
||||
@@ -80,6 +80,13 @@ class MockSerializer : public Empty {
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeBundleHashingEvent, (SNTStoredEvent *));
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeDiskAppeared, (NSDictionary *));
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeDiskDisappeared, (NSDictionary *));
|
||||
|
||||
MOCK_METHOD(
|
||||
std::vector<uint8_t>, SerializeFileAccess,
|
||||
(const std::string &policy_version, const std::string &policy_name,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedProcess &enriched_process,
|
||||
const std::string &target, FileAccessPolicyDecision decision));
|
||||
};
|
||||
|
||||
class MockWriter : public Null {
|
||||
@@ -128,11 +135,7 @@ class MockWriter : public Null {
|
||||
// Ensure all Logger::Log* methods call the serializer followed by the writer
|
||||
es_message_t msg;
|
||||
|
||||
// Note: In this test, `RetainMessage` isn't setup to return anything. This
|
||||
// means that the underlying `es_msg_` in the `Message` object is NULL, and
|
||||
// therefore no call to `ReleaseMessage` is ever made (hence no expectations).
|
||||
// Because we don't need to operate on the es_msg_, this simplifies the test.
|
||||
EXPECT_CALL(*mockESApi, RetainMessage);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
auto enrichedMsg = std::make_shared<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
@@ -156,7 +159,7 @@ class MockWriter : public Null {
|
||||
es_message_t msg;
|
||||
std::string_view hash = "this_is_my_test_hash";
|
||||
|
||||
EXPECT_CALL(*mockESApi, RetainMessage);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
EXPECT_CALL(*mockSerializer, SerializeAllowlist(testing::_, hash));
|
||||
EXPECT_CALL(*mockWriter, Write);
|
||||
|
||||
@@ -207,4 +210,24 @@ class MockWriter : public Null {
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
- (void)testLogFileAccess {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto mockSerializer = std::make_shared<MockSerializer>();
|
||||
auto mockWriter = std::make_shared<MockWriter>();
|
||||
es_message_t msg;
|
||||
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
EXPECT_CALL(*mockSerializer, SerializeFileAccess);
|
||||
EXPECT_CALL(*mockWriter, Write);
|
||||
|
||||
Logger(mockSerializer, mockWriter)
|
||||
.LogFileAccess("v1", "name", Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
"tgt", FileAccessPolicyDecision::kDenied);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
39
Source/santad/Logs/EndpointSecurity/MockLogger.h
Normal file
39
Source/santad/Logs/EndpointSecurity/MockLogger.h
Normal file
@@ -0,0 +1,39 @@
|
||||
/// Copyright 2022 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__LOGS_ENDPOINTSECURITY_MOCKLOGGER_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_MOCKLOGGER_H
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
|
||||
class MockLogger : public santa::santad::logs::endpoint_security::Logger {
|
||||
public:
|
||||
using Logger::Logger;
|
||||
|
||||
MockLogger() : Logger(nullptr, nullptr) {}
|
||||
|
||||
MOCK_METHOD(
|
||||
void, LogFileAccess,
|
||||
(const std::string &policy_version, const std::string &policy_name,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedProcess
|
||||
&enriched_process,
|
||||
const std::string &target, FileAccessPolicyDecision decision));
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -53,6 +53,12 @@ class BasicString : public Serializer {
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedProcess &enriched_process,
|
||||
const std::string &target, FileAccessPolicyDecision decision) override;
|
||||
|
||||
std::vector<uint8_t> SerializeAllowlist(
|
||||
const santa::santad::event_providers::endpoint_security::Message &,
|
||||
const std::string_view) override;
|
||||
|
||||
@@ -43,6 +43,7 @@ using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFork;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedLink;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedRename;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
@@ -110,6 +111,33 @@ std::string GetModeString(SNTClientMode mode) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetAccessTypeString(es_event_type_t event_type) {
|
||||
switch (event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_CLONE: return "CLONE";
|
||||
case ES_EVENT_TYPE_AUTH_COPYFILE: return "COPYFILE";
|
||||
case ES_EVENT_TYPE_AUTH_CREATE: return "CREATE";
|
||||
case ES_EVENT_TYPE_AUTH_EXCHANGEDATA: return "EXCHANGEDATA";
|
||||
case ES_EVENT_TYPE_AUTH_LINK: return "LINK";
|
||||
case ES_EVENT_TYPE_AUTH_OPEN: return "OPEN";
|
||||
case ES_EVENT_TYPE_AUTH_RENAME: return "RENAME";
|
||||
case ES_EVENT_TYPE_AUTH_TRUNCATE: return "TRUNCATE";
|
||||
case ES_EVENT_TYPE_AUTH_UNLINK: return "UNLINK";
|
||||
default: return "UNKNOWN_TYPE_" + std::to_string(event_type);
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetFileAccessPolicyDecisionString(FileAccessPolicyDecision decision) {
|
||||
switch (decision) {
|
||||
case FileAccessPolicyDecision::kNoPolicy: return "NO_POLICY";
|
||||
case FileAccessPolicyDecision::kDenied: return "DENIED";
|
||||
case FileAccessPolicyDecision::kDeniedInvalidSignature: return "DENIED_INVALID_SIGNATURE";
|
||||
case FileAccessPolicyDecision::kAllowed: return "ALLOWED";
|
||||
case FileAccessPolicyDecision::kAllowedReadAccess: return "ALLOWED_READ_ACCESS";
|
||||
case FileAccessPolicyDecision::kAllowedAuditOnly: return "AUDIT_ONLY";
|
||||
default: return "UNKNOWN_DECISION_" + std::to_string((int)decision);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void AppendProcess(std::string &str, const es_process_t *es_proc) {
|
||||
char bname[MAXPATHLEN];
|
||||
str.append("|pid=");
|
||||
@@ -383,6 +411,32 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeFileAccess(const std::string &policy_version,
|
||||
const std::string &policy_name,
|
||||
const Message &msg,
|
||||
const EnrichedProcess &enriched_process,
|
||||
const std::string &target,
|
||||
FileAccessPolicyDecision decision) {
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=FILE_ACCESS|policy_version=");
|
||||
str.append(policy_version);
|
||||
str.append("|policy_name=");
|
||||
str.append(policy_name);
|
||||
str.append("|path=");
|
||||
str.append(target);
|
||||
str.append("|access_type=");
|
||||
str.append(GetAccessTypeString(msg->event_type));
|
||||
str.append("|decision=");
|
||||
str.append(GetFileAccessPolicyDecisionString(decision));
|
||||
|
||||
AppendProcess(str, msg->process);
|
||||
AppendUserGroup(str, msg->process->audit_token, enriched_process.real_user(),
|
||||
enriched_process.real_group());
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeAllowlist(const Message &msg,
|
||||
const std::string_view hash) {
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
@@ -45,15 +45,19 @@ namespace santa::santad::logs::endpoint_security::serializers {
|
||||
extern std::string GetDecisionString(SNTEventState event_state);
|
||||
extern std::string GetReasonString(SNTEventState event_state);
|
||||
extern std::string GetModeString(SNTClientMode mode);
|
||||
extern std::string GetAccessTypeString(es_event_type_t event_type);
|
||||
extern std::string GetFileAccessPolicyDecisionString(FileAccessPolicyDecision decision);
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
using santa::santad::logs::endpoint_security::serializers::GetAccessTypeString;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetDecisionString;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetFileAccessPolicyDecisionString;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetModeString;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetReasonString;
|
||||
|
||||
std::string BasicStringSerializeMessage(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::shared_ptr<Serializer> bs = BasicString::Create(mockESApi, false);
|
||||
std::vector<uint8_t> ret = bs->SerializeMessage(Enricher().Enrich(Message(mockESApi, esMsg)));
|
||||
@@ -246,6 +250,57 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testGetAccessTypeString {
|
||||
std::map<es_event_type_t, std::string> accessTypeToString = {
|
||||
{ES_EVENT_TYPE_AUTH_OPEN, "OPEN"}, {ES_EVENT_TYPE_AUTH_LINK, "LINK"},
|
||||
{ES_EVENT_TYPE_AUTH_RENAME, "RENAME"}, {ES_EVENT_TYPE_AUTH_UNLINK, "UNLINK"},
|
||||
{ES_EVENT_TYPE_AUTH_CLONE, "CLONE"}, {ES_EVENT_TYPE_AUTH_EXCHANGEDATA, "EXCHANGEDATA"},
|
||||
{ES_EVENT_TYPE_AUTH_CREATE, "CREATE"}, {ES_EVENT_TYPE_AUTH_TRUNCATE, "TRUNCATE"},
|
||||
{ES_EVENT_TYPE_AUTH_COPYFILE, "COPYFILE"}, {(es_event_type_t)1234, "UNKNOWN_TYPE_1234"},
|
||||
};
|
||||
|
||||
for (const auto &kv : accessTypeToString) {
|
||||
XCTAssertCppStringEqual(GetAccessTypeString(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetFileAccessPolicyDecisionString {
|
||||
std::map<FileAccessPolicyDecision, std::string> policyDecisionToString = {
|
||||
{FileAccessPolicyDecision::kNoPolicy, "NO_POLICY"},
|
||||
{FileAccessPolicyDecision::kDenied, "DENIED"},
|
||||
{FileAccessPolicyDecision::kDeniedInvalidSignature, "DENIED_INVALID_SIGNATURE"},
|
||||
{FileAccessPolicyDecision::kAllowed, "ALLOWED"},
|
||||
{FileAccessPolicyDecision::kAllowedReadAccess, "ALLOWED_READ_ACCESS"},
|
||||
{FileAccessPolicyDecision::kAllowedAuditOnly, "AUDIT_ONLY"},
|
||||
{(FileAccessPolicyDecision)1234, "UNKNOWN_DECISION_1234"},
|
||||
};
|
||||
|
||||
for (const auto &kv : policyDecisionToString) {
|
||||
XCTAssertCppStringEqual(GetFileAccessPolicyDecisionString(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSerializeFileAccess {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::vector<uint8_t> ret =
|
||||
BasicString::Create(nullptr, false)
|
||||
->SerializeFileAccess("v1.0", "pol_name", Message(mockESApi, &esMsg),
|
||||
Enricher().Enrich(*esMsg.process), "file_target",
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
std::string want =
|
||||
"action=FILE_ACCESS|policy_version=v1.0|policy_name=pol_name|path=file_target|access_type=OPEN|"
|
||||
"decision=AUDIT_ONLY|pid=12|ppid=56|"
|
||||
"process=foo|processpath=foo|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeAllowlist {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
@@ -253,7 +308,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
esMsg.event.close.target = &file;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(mockESApi, false)
|
||||
->SerializeAllowlist(Message(mockESApi, &esMsg), "test_hash");
|
||||
|
||||
@@ -45,6 +45,12 @@ class Empty : public Serializer {
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedProcess &enriched_process,
|
||||
const std::string &target, FileAccessPolicyDecision decision) override;
|
||||
|
||||
std::vector<uint8_t> SerializeAllowlist(
|
||||
const santa::santad::event_providers::endpoint_security::Message &,
|
||||
const std::string_view) override;
|
||||
|
||||
@@ -20,6 +20,7 @@ using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFork;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedLink;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedRename;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
@@ -62,6 +63,14 @@ std::vector<uint8_t> Empty::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeFileAccess(const std::string &policy_version,
|
||||
const std::string &policy_name, const Message &msg,
|
||||
const EnrichedProcess &enriched_process,
|
||||
const std::string &target,
|
||||
FileAccessPolicyDecision decision) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeAllowlist(const Message &msg, const std::string_view hash) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -52,6 +52,12 @@ class Protobuf : public Serializer {
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedProcess &enriched_process,
|
||||
const std::string &target, FileAccessPolicyDecision decision) override;
|
||||
|
||||
std::vector<uint8_t> SerializeAllowlist(
|
||||
const santa::santad::event_providers::endpoint_security::Message &,
|
||||
const std::string_view) override;
|
||||
@@ -66,6 +72,9 @@ class Protobuf : public Serializer {
|
||||
::santa::pb::v1::SantaMessage *CreateDefaultProto(
|
||||
google::protobuf::Arena *arena,
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedEventType &msg);
|
||||
::santa::pb::v1::SantaMessage *CreateDefaultProto(
|
||||
google::protobuf::Arena *arena,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg);
|
||||
::santa::pb::v1::SantaMessage *CreateDefaultProto(google::protobuf::Arena *arena,
|
||||
struct timespec event_time,
|
||||
struct timespec processed_time);
|
||||
|
||||
@@ -160,6 +160,12 @@ static inline void EncodeFileInfo(::pbv1::FileInfo *pb_file, const es_file_t *es
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, std::string_view path,
|
||||
bool truncated) {
|
||||
EncodeString(pb_file->mutable_path(), path);
|
||||
pb_file->set_truncated(truncated);
|
||||
}
|
||||
|
||||
static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, const es_file_t *es_file) {
|
||||
EncodePath(pb_file->mutable_path(), es_file);
|
||||
pb_file->set_truncated(es_file->path_truncated);
|
||||
@@ -319,6 +325,32 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::FileAccess::AccessType GetAccessType(es_event_type_t event_type) {
|
||||
switch (event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_CLONE: return ::pbv1::FileAccess::ACCESS_TYPE_CLONE;
|
||||
case ES_EVENT_TYPE_AUTH_CREATE: return ::pbv1::FileAccess::ACCESS_TYPE_CREATE;
|
||||
case ES_EVENT_TYPE_AUTH_COPYFILE: return ::pbv1::FileAccess::ACCESS_TYPE_COPYFILE;
|
||||
case ES_EVENT_TYPE_AUTH_EXCHANGEDATA: return ::pbv1::FileAccess::ACCESS_TYPE_EXCHANGEDATA;
|
||||
case ES_EVENT_TYPE_AUTH_LINK: return ::pbv1::FileAccess::ACCESS_TYPE_LINK;
|
||||
case ES_EVENT_TYPE_AUTH_OPEN: return ::pbv1::FileAccess::ACCESS_TYPE_OPEN;
|
||||
case ES_EVENT_TYPE_AUTH_RENAME: return ::pbv1::FileAccess::ACCESS_TYPE_RENAME;
|
||||
case ES_EVENT_TYPE_AUTH_TRUNCATE: return ::pbv1::FileAccess::ACCESS_TYPE_TRUNCATE;
|
||||
case ES_EVENT_TYPE_AUTH_UNLINK: return ::pbv1::FileAccess::ACCESS_TYPE_UNLINK;
|
||||
default: return ::pbv1::FileAccess::ACCESS_TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::FileAccess::PolicyDecision GetPolicyDecision(FileAccessPolicyDecision decision) {
|
||||
switch (decision) {
|
||||
case FileAccessPolicyDecision::kDenied: return ::pbv1::FileAccess::POLICY_DECISION_DENIED;
|
||||
case FileAccessPolicyDecision::kDeniedInvalidSignature:
|
||||
return ::pbv1::FileAccess::POLICY_DECISION_DENIED_INVALID_SIGNATURE;
|
||||
case FileAccessPolicyDecision::kAllowedAuditOnly:
|
||||
return ::pbv1::FileAccess::POLICY_DECISION_ALLOWED_AUDIT_ONLY;
|
||||
default: return ::pbv1::FileAccess::POLICY_DECISION_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, struct timespec event_time,
|
||||
struct timespec processed_time) {
|
||||
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
|
||||
@@ -336,6 +368,13 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
return CreateDefaultProto(arena, msg.es_msg().time, msg.enrichment_time());
|
||||
}
|
||||
|
||||
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, const Message &msg) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
|
||||
return CreateDefaultProto(arena, msg->time, ts);
|
||||
}
|
||||
|
||||
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
@@ -532,6 +571,28 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeFileAccess(const std::string &policy_version,
|
||||
const std::string &policy_name,
|
||||
const Message &msg,
|
||||
const EnrichedProcess &enriched_process,
|
||||
const std::string &target,
|
||||
FileAccessPolicyDecision decision) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::FileAccess *file_access = santa_msg->mutable_file_access();
|
||||
|
||||
EncodeProcessInfo(file_access->mutable_instigator(), msg->version, msg->process,
|
||||
enriched_process);
|
||||
EncodeFileInfoLight(file_access->mutable_target(), target, false);
|
||||
EncodeString(file_access->mutable_policy_version(), policy_version);
|
||||
EncodeString(file_access->mutable_policy_name(), policy_name);
|
||||
file_access->set_access_type(GetAccessType(msg->event_type));
|
||||
file_access->set_policy_decision(GetPolicyDecision(decision));
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeAllowlist(const Message &msg, const std::string_view hash) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
|
||||
|
||||
@@ -61,12 +61,16 @@ extern ::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state);
|
||||
extern ::pbv1::Execution::Reason GetReasonEnum(SNTEventState event_state);
|
||||
extern ::pbv1::Execution::Mode GetModeEnum(SNTClientMode mode);
|
||||
extern ::pbv1::FileDescriptor::FDType GetFileDescriptorType(uint32_t fdtype);
|
||||
extern ::pbv1::FileAccess::AccessType GetAccessType(es_event_type_t event_type);
|
||||
extern ::pbv1::FileAccess::PolicyDecision GetPolicyDecision(FileAccessPolicyDecision decision);
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
using santa::santad::logs::endpoint_security::serializers::EncodeExitStatus;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetAccessType;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetDecisionEnum;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetFileDescriptorType;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetModeEnum;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetPolicyDecision;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetReasonEnum;
|
||||
|
||||
JsonPrintOptions DefaultJsonPrintOptions() {
|
||||
@@ -143,6 +147,7 @@ const google::protobuf::Message &SantaMessageEvent(const ::pbv1::SantaMessage &s
|
||||
case ::pbv1::SantaMessage::kDisk: return santaMsg.disk();
|
||||
case ::pbv1::SantaMessage::kBundle: return santaMsg.bundle();
|
||||
case ::pbv1::SantaMessage::kAllowlist: return santaMsg.allowlist();
|
||||
case ::pbv1::SantaMessage::kFileAccess: return santaMsg.file_access();
|
||||
case ::pbv1::SantaMessage::EVENT_NOT_SET:
|
||||
XCTFail(@"Protobuf message SantaMessage did not set an 'event' field");
|
||||
OS_FALLTHROUGH;
|
||||
@@ -195,7 +200,7 @@ void SerializeAndCheck(es_event_type_t eventType,
|
||||
esMsg.process->tty = &ttyFile;
|
||||
esMsg.version = cur_version;
|
||||
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
messageSetup(mockESApi, &esMsg);
|
||||
|
||||
@@ -214,6 +219,46 @@ void SerializeAndCheck(es_event_type_t eventType,
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
void SerializeAndCheckNonESEvents(
|
||||
es_event_type_t eventType, NSString *filename,
|
||||
void (^messageSetup)(std::shared_ptr<MockEndpointSecurityAPI>, es_message_t *),
|
||||
std::vector<uint8_t> (^RunSerializer)(std::shared_ptr<Serializer> serializer,
|
||||
const Message &msg)) {
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
|
||||
|
||||
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
|
||||
cur_version++) {
|
||||
if (cur_version == 3) {
|
||||
// Note: Version 3 was only in a macOS beta.
|
||||
continue;
|
||||
}
|
||||
|
||||
es_file_t procFile = MakeESFile("foo", MakeStat(100));
|
||||
es_file_t ttyFile = MakeESFile("footty", MakeStat(200));
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(eventType, &proc);
|
||||
esMsg.process->tty = &ttyFile;
|
||||
esMsg.version = cur_version;
|
||||
|
||||
messageSetup(mockESApi, &esMsg);
|
||||
|
||||
std::vector<uint8_t> vec = RunSerializer(bs, Message(mockESApi, &esMsg));
|
||||
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
std::string got = ConvertMessageToJsonString(santaMsg);
|
||||
NSString *wantData = LoadTestJson(filename, esMsg.version);
|
||||
|
||||
XCTAssertEqualObjects([NSString stringWithUTF8String:got.c_str()], wantData);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
@interface ProtobufTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property id mockDecisionCache;
|
||||
@@ -489,43 +534,68 @@ void SerializeAndCheck(es_event_type_t eventType,
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeAllowlist {
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
- (void)testGetAccessType {
|
||||
std::map<es_event_type_t, ::pbv1::FileAccess::AccessType> eventTypeToAccessType = {
|
||||
{ES_EVENT_TYPE_AUTH_CLONE, ::pbv1::FileAccess::ACCESS_TYPE_CLONE},
|
||||
{ES_EVENT_TYPE_AUTH_COPYFILE, ::pbv1::FileAccess::ACCESS_TYPE_COPYFILE},
|
||||
{ES_EVENT_TYPE_AUTH_CREATE, ::pbv1::FileAccess::ACCESS_TYPE_CREATE},
|
||||
{ES_EVENT_TYPE_AUTH_EXCHANGEDATA, ::pbv1::FileAccess::ACCESS_TYPE_EXCHANGEDATA},
|
||||
{ES_EVENT_TYPE_AUTH_LINK, ::pbv1::FileAccess::ACCESS_TYPE_LINK},
|
||||
{ES_EVENT_TYPE_AUTH_OPEN, ::pbv1::FileAccess::ACCESS_TYPE_OPEN},
|
||||
{ES_EVENT_TYPE_AUTH_RENAME, ::pbv1::FileAccess::ACCESS_TYPE_RENAME},
|
||||
{ES_EVENT_TYPE_AUTH_TRUNCATE, ::pbv1::FileAccess::ACCESS_TYPE_TRUNCATE},
|
||||
{ES_EVENT_TYPE_AUTH_UNLINK, ::pbv1::FileAccess::ACCESS_TYPE_UNLINK},
|
||||
{(es_event_type_t)1234, ::pbv1::FileAccess::ACCESS_TYPE_UNKNOWN},
|
||||
};
|
||||
|
||||
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
|
||||
cur_version++) {
|
||||
if (cur_version == 3) {
|
||||
// Note: Version 3 was only in a macOS beta.
|
||||
continue;
|
||||
}
|
||||
|
||||
es_file_t procFile = MakeESFile("foo", MakeStat(100));
|
||||
es_file_t ttyFile = MakeESFile("footty", MakeStat(200));
|
||||
es_file_t closeFile = MakeESFile("close_file", MakeStat(300));
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
|
||||
esMsg.process->tty = &ttyFile;
|
||||
esMsg.version = cur_version;
|
||||
esMsg.event.close.modified = true;
|
||||
esMsg.event.close.target = &closeFile;
|
||||
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
|
||||
|
||||
std::vector<uint8_t> vec = bs->SerializeAllowlist(Message(mockESApi, &esMsg), "hash_value");
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
|
||||
NSString *wantData = LoadTestJson(@"allowlist.json", esMsg.version);
|
||||
std::string got = ConvertMessageToJsonString(santaMsg);
|
||||
|
||||
XCTAssertEqualObjects([NSString stringWithUTF8String:got.c_str()], wantData);
|
||||
for (const auto &kv : eventTypeToAccessType) {
|
||||
XCTAssertEqual(GetAccessType(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
- (void)testGetPolicyDecision {
|
||||
std::map<FileAccessPolicyDecision, ::pbv1::FileAccess::PolicyDecision> policyDecisionEnumToProto =
|
||||
{
|
||||
{FileAccessPolicyDecision::kNoPolicy, ::pbv1::FileAccess::POLICY_DECISION_UNKNOWN},
|
||||
{FileAccessPolicyDecision::kDenied, ::pbv1::FileAccess::POLICY_DECISION_DENIED},
|
||||
{FileAccessPolicyDecision::kDeniedInvalidSignature,
|
||||
::pbv1::FileAccess::POLICY_DECISION_DENIED_INVALID_SIGNATURE},
|
||||
{FileAccessPolicyDecision::kAllowed, ::pbv1::FileAccess::POLICY_DECISION_UNKNOWN},
|
||||
{FileAccessPolicyDecision::kAllowedReadAccess, ::pbv1::FileAccess::POLICY_DECISION_UNKNOWN},
|
||||
{FileAccessPolicyDecision::kAllowedAuditOnly,
|
||||
::pbv1::FileAccess::POLICY_DECISION_ALLOWED_AUDIT_ONLY},
|
||||
{(FileAccessPolicyDecision)1234, ::pbv1::FileAccess::POLICY_DECISION_UNKNOWN},
|
||||
};
|
||||
|
||||
for (const auto &kv : policyDecisionEnumToProto) {
|
||||
XCTAssertEqual(GetPolicyDecision(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSerializeFileAccess {
|
||||
__block es_file_t openFile = MakeESFile("open_file", MakeStat(300));
|
||||
SerializeAndCheckNonESEvents(
|
||||
ES_EVENT_TYPE_AUTH_OPEN, @"file_access.json",
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.open.file = &openFile;
|
||||
},
|
||||
^std::vector<uint8_t>(std::shared_ptr<Serializer> serializer, const Message &msg) {
|
||||
return serializer->SerializeFileAccess("policy_version", "policy_name", msg,
|
||||
Enricher().Enrich(*msg->process), "target",
|
||||
FileAccessPolicyDecision::kDenied);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeAllowlist {
|
||||
__block es_file_t closeFile = MakeESFile("close_file", MakeStat(300));
|
||||
SerializeAndCheckNonESEvents(
|
||||
ES_EVENT_TYPE_NOTIFY_CLOSE, @"allowlist.json",
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.close.target = &closeFile;
|
||||
},
|
||||
^std::vector<uint8_t>(std::shared_ptr<Serializer> serializer, const Message &msg) {
|
||||
return serializer->SerializeAllowlist(msg, "hash_value");
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeBundleHashingEvent {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
|
||||
@class SNTStoredEvent;
|
||||
@@ -57,6 +58,12 @@ class Serializer {
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) = 0;
|
||||
|
||||
virtual std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedProcess &enriched_process,
|
||||
const std::string &target, FileAccessPolicyDecision decision) = 0;
|
||||
|
||||
virtual std::vector<uint8_t> SerializeAllowlist(
|
||||
const santa::santad::event_providers::endpoint_security::Message &, const std::string_view) = 0;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ using santa::santad::logs::endpoint_security::serializers::Utilities::GetAllowLi
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
{
|
||||
esMsg.event.close.target = &closeTargetFile;
|
||||
|
||||
@@ -49,6 +49,10 @@ class File : public Writer, public std::enable_shared_from_this<File> {
|
||||
void OpenFileHandle();
|
||||
void WatchLogFile();
|
||||
void FlushBuffer();
|
||||
bool ShouldFlush();
|
||||
|
||||
void EnsureCapacity(size_t additional_bytes);
|
||||
void CopyData(const std::vector<uint8_t> &bytes);
|
||||
|
||||
std::vector<uint8_t> buffer_;
|
||||
size_t batch_size_bytes_;
|
||||
@@ -57,6 +61,12 @@ class File : public Writer, public std::enable_shared_from_this<File> {
|
||||
dispatch_source_t watch_source_;
|
||||
NSString *path_;
|
||||
NSFileHandle *file_handle_;
|
||||
|
||||
// Used to manually track the size of valid data in the `buffer_`.
|
||||
// Benchmarking showed a large amount of time clearing the buffer after
|
||||
// flushes, but that isn't very necessary. Instead we can manually track the
|
||||
// `end` of the buffer and skip clearing the data.
|
||||
size_t buffer_offset_ = 0;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user