mirror of
https://github.com/google/santa.git
synced 2026-01-15 09:17:59 -05:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7502bc247f | ||
|
|
cf4dab55e0 | ||
|
|
e43ad30d4e | ||
|
|
d8928ac320 | ||
|
|
ac1c9d8b05 | ||
|
|
9b184ed4fb | ||
|
|
67883c5200 | ||
|
|
8e1e155c23 | ||
|
|
fb6aa850b3 | ||
|
|
7f06b8c11a | ||
|
|
978b33e450 | ||
|
|
f00ad32edd | ||
|
|
7b0d2fdbb8 | ||
|
|
1672e52b7b | ||
|
|
6cca5ab27d | ||
|
|
7e4af5e337 | ||
|
|
5ea4431901 | ||
|
|
b53818f556 | ||
|
|
0f5e551345 | ||
|
|
51b0f7146d | ||
|
|
f5882b3146 | ||
|
|
59c146b4af | ||
|
|
aaa2b0e259 | ||
|
|
9c6fd0677f | ||
|
|
344a35aaf6 | ||
|
|
45e36fa501 | ||
|
|
d5a7c5f1fa | ||
|
|
22aca6b505 | ||
|
|
375f7bd9cc | ||
|
|
7d58665e87 | ||
|
|
3b2d02f38d | ||
|
|
57fc2b0253 | ||
|
|
262adfecbd | ||
|
|
1606657bb3 | ||
|
|
b379819cfa | ||
|
|
b9f6005411 | ||
|
|
e31aa5cf39 | ||
|
|
77d191ae26 | ||
|
|
160195a1d4 | ||
|
|
f2ce92650b | ||
|
|
e89cdbcf64 | ||
|
|
6a697e00ea | ||
|
|
74d8fe30d1 | ||
|
|
7513c75f88 | ||
|
|
9bee43130e | ||
|
|
7fa23d4b97 | ||
|
|
42eb0a3669 | ||
|
|
1ea26f0ac9 | ||
|
|
c35e9978d3 | ||
|
|
e4c0d56bb6 | ||
|
|
908b1bcabe | ||
|
|
64e81bedc6 | ||
|
|
5dfab22fa7 | ||
|
|
5248e2a7eb | ||
|
|
e8db89c57c |
@@ -1 +1 @@
|
||||
6.3.2
|
||||
7.0.0
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-11, macos-12, macos-13]
|
||||
os: [macos-11, macos-12, macos-13, macos-14]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
@@ -31,14 +31,14 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-11, macos-12, macos-13]
|
||||
os: [macos-11, macos-12, macos-13, macos-14]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Run All Tests
|
||||
run: bazel test :unit_tests --define=SANTA_BUILD_TYPE=adhoc --test_output=errors
|
||||
test_coverage:
|
||||
runs-on: macos-11
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Generate test coverage
|
||||
|
||||
18
.github/workflows/e2e.yml
vendored
18
.github/workflows/e2e.yml
vendored
@@ -6,11 +6,29 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update_vm:
|
||||
runs-on: e2e-host
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Update VM
|
||||
env:
|
||||
GCS_KEY: ${{ secrets.GCS_SERVICE_ACCOUNT_KEY }}
|
||||
run: |
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json
|
||||
echo "${GCS_KEY}" > ${GOOGLE_APPLICATION_CREDENTIALS}
|
||||
function cleanup {
|
||||
rm /tmp/gcp.json
|
||||
}
|
||||
trap cleanup EXIT
|
||||
python3 Testing/integration/actions/update_vm.py macOS_14.bundle.tar.gz
|
||||
|
||||
start_vm:
|
||||
runs-on: e2e-host
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Start VM
|
||||
env:
|
||||
RUNNER_REG_TOKEN: ${{ secrets.RUNNER_REG_TOKEN }}
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_14.bundle.tar.gz
|
||||
|
||||
integration:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,7 +2,7 @@
|
||||
*.profraw
|
||||
*.provisionprofile
|
||||
bazel-*
|
||||
Pods
|
||||
MODULE.bazel.lock
|
||||
Santa.xcodeproj/*
|
||||
Santa.xcworkspace/*
|
||||
CoverageData/*
|
||||
|
||||
5
.pyink-config
Normal file
5
.pyink-config
Normal file
@@ -0,0 +1,5 @@
|
||||
[tool.pyink]
|
||||
pyink = true
|
||||
line-length = 80
|
||||
pyink-indentation = 2
|
||||
pyink-use-majority-quotes = true
|
||||
429
.pylintrc
429
.pylintrc
@@ -1,429 +0,0 @@
|
||||
# This Pylint rcfile contains a best-effort configuration to uphold the
|
||||
# best-practices and style described in the Google Python style guide:
|
||||
# https://google.github.io/styleguide/pyguide.html
|
||||
#
|
||||
# Its canonical open-source location is:
|
||||
# https://google.github.io/styleguide/pylintrc
|
||||
|
||||
[MASTER]
|
||||
|
||||
# Files or directories to be skipped. They should be base names, not paths.
|
||||
ignore=third_party
|
||||
|
||||
# Files or directories matching the regex patterns are skipped. The regex
|
||||
# matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=no
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Use multiple processes to speed up Pylint.
|
||||
jobs=4
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||
confidence=
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
#enable=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
disable=abstract-method,
|
||||
apply-builtin,
|
||||
arguments-differ,
|
||||
attribute-defined-outside-init,
|
||||
backtick,
|
||||
bad-option-value,
|
||||
basestring-builtin,
|
||||
buffer-builtin,
|
||||
c-extension-no-member,
|
||||
consider-using-enumerate,
|
||||
cmp-builtin,
|
||||
cmp-method,
|
||||
coerce-builtin,
|
||||
coerce-method,
|
||||
delslice-method,
|
||||
div-method,
|
||||
duplicate-code,
|
||||
eq-without-hash,
|
||||
execfile-builtin,
|
||||
file-builtin,
|
||||
filter-builtin-not-iterating,
|
||||
fixme,
|
||||
getslice-method,
|
||||
global-statement,
|
||||
hex-method,
|
||||
idiv-method,
|
||||
implicit-str-concat,
|
||||
import-error,
|
||||
import-self,
|
||||
import-star-module-level,
|
||||
inconsistent-return-statements,
|
||||
input-builtin,
|
||||
intern-builtin,
|
||||
invalid-str-codec,
|
||||
locally-disabled,
|
||||
long-builtin,
|
||||
long-suffix,
|
||||
map-builtin-not-iterating,
|
||||
misplaced-comparison-constant,
|
||||
missing-function-docstring,
|
||||
metaclass-assignment,
|
||||
next-method-called,
|
||||
next-method-defined,
|
||||
no-absolute-import,
|
||||
no-else-break,
|
||||
no-else-continue,
|
||||
no-else-raise,
|
||||
no-else-return,
|
||||
no-init, # added
|
||||
no-member,
|
||||
no-name-in-module,
|
||||
no-self-use,
|
||||
nonzero-method,
|
||||
oct-method,
|
||||
old-division,
|
||||
old-ne-operator,
|
||||
old-octal-literal,
|
||||
old-raise-syntax,
|
||||
parameter-unpacking,
|
||||
print-statement,
|
||||
raising-string,
|
||||
range-builtin-not-iterating,
|
||||
raw_input-builtin,
|
||||
rdiv-method,
|
||||
reduce-builtin,
|
||||
relative-import,
|
||||
reload-builtin,
|
||||
round-builtin,
|
||||
setslice-method,
|
||||
signature-differs,
|
||||
standarderror-builtin,
|
||||
suppressed-message,
|
||||
sys-max-int,
|
||||
too-few-public-methods,
|
||||
too-many-ancestors,
|
||||
too-many-arguments,
|
||||
too-many-boolean-expressions,
|
||||
too-many-branches,
|
||||
too-many-instance-attributes,
|
||||
too-many-locals,
|
||||
too-many-nested-blocks,
|
||||
too-many-public-methods,
|
||||
too-many-return-statements,
|
||||
too-many-statements,
|
||||
trailing-newlines,
|
||||
unichr-builtin,
|
||||
unicode-builtin,
|
||||
unnecessary-pass,
|
||||
unpacking-in-except,
|
||||
useless-else-on-loop,
|
||||
useless-object-inheritance,
|
||||
useless-suppression,
|
||||
using-cmp-argument,
|
||||
wrong-import-order,
|
||||
xrange-builtin,
|
||||
zip-builtin-not-iterating,
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||
# (visual studio) and html. You can also give a reporter class, eg
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=no
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details
|
||||
#msg-template=
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names=main,_
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name
|
||||
include-naming-hint=no
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
|
||||
|
||||
# Regular expression matching correct function names
|
||||
function-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
|
||||
|
||||
# Regular expression matching correct variable names
|
||||
variable-rgx=^[a-z][a-z0-9_]*$
|
||||
|
||||
# Regular expression matching correct constant names
|
||||
const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
|
||||
|
||||
# Regular expression matching correct attribute names
|
||||
attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
|
||||
|
||||
# Regular expression matching correct argument names
|
||||
argument-rgx=^[a-z][a-z0-9_]*$
|
||||
|
||||
# Regular expression matching correct class attribute names
|
||||
class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
|
||||
|
||||
# Regular expression matching correct inline iteration names
|
||||
inlinevar-rgx=^[a-z][a-z0-9_]*$
|
||||
|
||||
# Regular expression matching correct class names
|
||||
class-rgx=^_?[A-Z][a-zA-Z0-9]*$
|
||||
|
||||
# Regular expression matching correct module names
|
||||
module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$
|
||||
|
||||
# Regular expression matching correct method names
|
||||
method-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=10
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=80
|
||||
|
||||
# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt
|
||||
# lines made too long by directives to pytype.
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=(?x)(
|
||||
^\s*(\#\ )?<?https?://\S+>?$|
|
||||
^\s*(from\s+\S+\s+)?import\s+.+$)
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=yes
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=99999
|
||||
|
||||
# String used as indentation unit. The internal Google style guide mandates 2
|
||||
# spaces. Google's externaly-published style guide says 4, consistent with
|
||||
# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google
|
||||
# projects (like TensorFlow).
|
||||
indent-string=' '
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=TODO
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||
# character used as a quote delimiter is used inconsistently within a module.
|
||||
check-quote-consistency=yes
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||
# not used).
|
||||
dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,_cb
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format
|
||||
logging-modules=logging,absl.logging,tensorflow.io.logging
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=regsub,
|
||||
TERMIOS,
|
||||
Bastion,
|
||||
rexec,
|
||||
sets
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant, absl
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls,
|
||||
class_
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=StandardError,
|
||||
Exception,
|
||||
BaseException
|
||||
4
BUILD
4
BUILD
@@ -1,7 +1,9 @@
|
||||
load("@build_bazel_rules_apple//apple:versioning.bzl", "apple_bundle_version")
|
||||
load("//:helper.bzl", "run_command")
|
||||
|
||||
package(default_visibility = ["//:santa_package_group"])
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
|
||||
@@ -53,11 +53,10 @@ readonly RELEASE_NAME="santa-$(/usr/bin/defaults read "${INPUT_APP}/Contents/Inf
|
||||
readonly SCRATCH=$(/usr/bin/mktemp -d "${TMPDIR}/santa-"XXXXXX)
|
||||
readonly APP_PKG_ROOT="${SCRATCH}/app_pkg_root"
|
||||
readonly APP_PKG_SCRIPTS="${SCRATCH}/pkg_scripts"
|
||||
readonly ENTITLEMENTS="${SCRATCH}/entitlements"
|
||||
|
||||
readonly SCRIPT_PATH="$(/usr/bin/dirname -- ${BASH_SOURCE[0]})"
|
||||
|
||||
/bin/mkdir -p "${APP_PKG_ROOT}" "${APP_PKG_SCRIPTS}" "${ENTITLEMENTS}"
|
||||
/bin/mkdir -p "${APP_PKG_ROOT}" "${APP_PKG_SCRIPTS}"
|
||||
|
||||
readonly DMG_PATH="${ARTIFACTS_DIR}/${RELEASE_NAME}.dmg"
|
||||
readonly TAR_PATH="${ARTIFACTS_DIR}/${RELEASE_NAME}.tar.gz"
|
||||
@@ -65,19 +64,9 @@ readonly TAR_PATH="${ARTIFACTS_DIR}/${RELEASE_NAME}.tar.gz"
|
||||
# Sign all of binaries/bundles. Maintain inside-out ordering where necessary
|
||||
for ARTIFACT in "${INPUT_SANTACTL}" "${INPUT_SANTABS}" "${INPUT_SANTAMS}" "${INPUT_SANTASS}" "${INPUT_SYSX}" "${INPUT_APP}"; do
|
||||
BN=$(/usr/bin/basename "${ARTIFACT}")
|
||||
EN="${ENTITLEMENTS}/${BN}.entitlements"
|
||||
|
||||
echo "extracting ${BN} entitlements"
|
||||
/usr/bin/codesign -d --entitlements "${EN}" "${ARTIFACT}"
|
||||
if [[ -s "${EN}" ]]; then
|
||||
EN="--entitlements ${EN}"
|
||||
else
|
||||
EN=""
|
||||
fi
|
||||
|
||||
echo "codesigning ${BN}"
|
||||
/usr/bin/codesign --sign "${SIGNING_IDENTITY}" --keychain "${SIGNING_KEYCHAIN}" \
|
||||
${EN} --timestamp --force --generate-entitlement-der \
|
||||
--preserve-metadata=entitlements --timestamp --force --generate-entitlement-der \
|
||||
--options library,kill,runtime "${ARTIFACT}"
|
||||
done
|
||||
|
||||
|
||||
55
MODULE.bazel
Normal file
55
MODULE.bazel
Normal file
@@ -0,0 +1,55 @@
|
||||
module(name = "santa")
|
||||
|
||||
bazel_dep(name = "apple_support", version = "1.15.1", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "abseil-cpp", version = "20240116.1", repo_name = "com_google_absl")
|
||||
bazel_dep(name = "rules_python", version = "0.31.0")
|
||||
bazel_dep(name = "rules_cc", version = "0.0.9")
|
||||
bazel_dep(name = "rules_apple", version = "3.5.0", repo_name = "build_bazel_rules_apple")
|
||||
bazel_dep(name = "rules_swift", version = "1.18.0", repo_name = "build_bazel_rules_swift")
|
||||
bazel_dep(name = "rules_fuzzing", version = "0.5.1")
|
||||
bazel_dep(name = "protobuf", version = "main", repo_name = "com_google_protobuf")
|
||||
git_override(
|
||||
module_name = "protobuf",
|
||||
commit = "21d75f861cdbc03b0a6b235a9ccf3ba0e1f09b32",
|
||||
remote = "https://github.com/protocolbuffers/protobuf.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "googletest", version = "1.14.0.bcr.1", repo_name = "com_google_googletest")
|
||||
bazel_dep(name = "molcertificate", version = "2.1", repo_name = "MOLCertificate")
|
||||
git_override(
|
||||
module_name = "molcertificate",
|
||||
commit = "34f0ccf68a34a07cc636ada89057c529f90bec3a",
|
||||
remote = "https://github.com/google/macops-molcertificate.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "molauthenticatingurlsession", version = "3.0", repo_name = "MOLAuthenticatingURLSession")
|
||||
git_override(
|
||||
module_name = "molauthenticatingurlsession",
|
||||
commit = "0a50a67f29d635a4012981714c1dedef9ac25fe6",
|
||||
remote = "https://github.com/google/macops-molauthenticatingurlsession.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "molcodesignchecker", version = "3.0", repo_name = "MOLCodesignChecker")
|
||||
git_override(
|
||||
module_name = "molcodesignchecker",
|
||||
commit = "5060bcc8baa90bae3b0ca705d14850328bbbec53",
|
||||
remote = "https://github.com/google/macops-molcodesignchecker.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "molxpcconnection", version = "2.1", repo_name = "MOLXPCConnection")
|
||||
git_override(
|
||||
module_name = "molxpcconnection",
|
||||
commit = "da816dc49becac96d941ef6a5c4153ed39d1fe7c",
|
||||
remote = "https://github.com/russellhancox/macops-molxpcconnection.git",
|
||||
)
|
||||
|
||||
non_module_deps = use_extension("//:non_module_deps.bzl", "non_module_deps")
|
||||
use_repo(non_module_deps, "FMDB")
|
||||
use_repo(non_module_deps, "OCMock")
|
||||
|
||||
bazel_dep(name = "hedron_compile_commands", dev_dependency = True)
|
||||
git_override(
|
||||
module_name = "hedron_compile_commands",
|
||||
commit = "0e990032f3c5a866e72615cf67e5ce22186dcb97",
|
||||
remote = "https://github.com/hedronvision/bazel-compile-commands-extractor.git",
|
||||
)
|
||||
@@ -21,7 +21,7 @@ It is named Santa because it keeps track of binaries that are naughty or nice.
|
||||
# Docs
|
||||
|
||||
The Santa docs are stored in the
|
||||
[Docs](https://github.com/google/santa/blob/main/docs) directory and published
|
||||
[Docs](https://github.com/google/santa/blob/main/docs) directory and are published
|
||||
at https://santa.dev.
|
||||
|
||||
The docs include deployment options, details on how parts of Santa work and
|
||||
|
||||
@@ -11,6 +11,7 @@ proto_library(
|
||||
name = "santa_proto",
|
||||
srcs = ["santa.proto"],
|
||||
deps = [
|
||||
"//Source/santad/ProcessTree:process_tree_proto",
|
||||
"@com_google_protobuf//:any_proto",
|
||||
"@com_google_protobuf//:timestamp_proto",
|
||||
],
|
||||
@@ -209,7 +210,7 @@ objc_library(
|
||||
],
|
||||
deps = [
|
||||
":CertificateHelpers",
|
||||
"@MOLCertificate",
|
||||
":SNTStoredEvent",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -263,6 +264,7 @@ objc_library(
|
||||
hdrs = ["SNTFileInfo.h"],
|
||||
deps = [
|
||||
":SNTLogging",
|
||||
":SantaVnode",
|
||||
"@FMDB",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
@@ -312,6 +314,12 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTRuleIdentifiers",
|
||||
srcs = ["SNTRuleIdentifiers.m"],
|
||||
hdrs = ["SNTRuleIdentifiers.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTStoredEvent",
|
||||
srcs = ["SNTStoredEvent.m"],
|
||||
@@ -377,6 +385,7 @@ objc_library(
|
||||
":SNTCommonEnums",
|
||||
":SNTConfigurator",
|
||||
":SNTRule",
|
||||
":SNTRuleIdentifiers",
|
||||
":SNTStoredEvent",
|
||||
":SNTXPCUnprivilegedControlInterface",
|
||||
"@MOLCodesignChecker",
|
||||
@@ -419,6 +428,7 @@ objc_library(
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
":SNTRule",
|
||||
":SNTRuleIdentifiers",
|
||||
":SNTStoredEvent",
|
||||
":SNTXPCBundleServiceInterface",
|
||||
":SantaVnode",
|
||||
@@ -468,13 +478,16 @@ santa_unit_test(
|
||||
name = "SNTBlockMessageTest",
|
||||
srcs = ["SNTBlockMessageTest.m"],
|
||||
deps = [
|
||||
":SNTBlockMessage",
|
||||
":SNTBlockMessage_SantaGUI",
|
||||
":SNTConfigurator",
|
||||
":SNTFileAccessEvent",
|
||||
":SNTStoredEvent",
|
||||
":SNTSystemInfo",
|
||||
"@OCMock",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"AppKit",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTSystemInfo.h"
|
||||
|
||||
static id ValueOrNull(id value) {
|
||||
return value ?: [NSNull null];
|
||||
}
|
||||
|
||||
@implementation SNTBlockMessage
|
||||
|
||||
+ (NSAttributedString *)formatMessage:(NSString *)message {
|
||||
@@ -52,7 +56,11 @@
|
||||
|
||||
#ifdef SANTAGUI
|
||||
NSData *htmlData = [fullHTML dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return [[NSAttributedString alloc] initWithHTML:htmlData documentAttributes:NULL];
|
||||
NSDictionary *options = @{
|
||||
NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType,
|
||||
NSCharacterEncodingDocumentAttribute : @(NSUTF8StringEncoding),
|
||||
};
|
||||
return [[NSAttributedString alloc] initWithHTML:htmlData options:options documentAttributes:NULL];
|
||||
#else
|
||||
NSString *strippedHTML = [self stringFromHTML:fullHTML];
|
||||
if (!strippedHTML) {
|
||||
@@ -123,35 +131,79 @@
|
||||
}
|
||||
|
||||
+ (NSString *)replaceFormatString:(NSString *)str
|
||||
withDict:(NSDictionary<NSString *, NSString * (^)()> *)replacements {
|
||||
withDict:(NSDictionary<NSString *, NSString *> *)replacements {
|
||||
__block NSString *formatStr = str;
|
||||
|
||||
[replacements
|
||||
enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString * (^computeValue)(), BOOL *stop) {
|
||||
NSString *value = computeValue();
|
||||
if (value) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:key withString:value];
|
||||
}
|
||||
}];
|
||||
[replacements enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) {
|
||||
if ((id)value != [NSNull null]) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:key withString:value];
|
||||
}
|
||||
}];
|
||||
|
||||
return formatStr;
|
||||
}
|
||||
|
||||
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
|
||||
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
|
||||
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
|
||||
// The following "format strings" will be replaced in the URL, if they are present:
|
||||
//
|
||||
// The following "format strings" will be replaced in the URL provided by
|
||||
// `+eventDetailURLForEvent:customURL:templateMapping:`.
|
||||
//
|
||||
// %file_identifier% - The SHA-256 of the binary being executed.
|
||||
// %bundle_or_file_identifier% - The hash of the bundle containing this file or the file itself,
|
||||
// if no bundle hash is present.
|
||||
// %file_bundle_id% - The bundle id of the binary, if any.
|
||||
// %team_id% - The Team ID if present in the signature information.
|
||||
// %signing_id% - The Signing ID if present in the signature information.
|
||||
// %cdhash% - If signed, the CDHash.
|
||||
// %username% - The executing user's name.
|
||||
// %machine_id% - The configured machine ID for this host.
|
||||
// %hostname% - The machine's FQDN.
|
||||
// %uuid% - The machine's UUID.
|
||||
// %serial% - The machine's serial number.
|
||||
//
|
||||
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url {
|
||||
+ (NSDictionary *)eventDetailTemplateMappingForEvent:(SNTStoredEvent *)event {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
return @{
|
||||
@"%file_sha%" : ValueOrNull(event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil),
|
||||
@"%file_identifier%" : ValueOrNull(event.fileSHA256),
|
||||
@"%bundle_or_file_identifier%" :
|
||||
ValueOrNull(event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil),
|
||||
@"%username%" : ValueOrNull(event.executingUser),
|
||||
@"%file_bundle_id%" : ValueOrNull(event.fileBundleID),
|
||||
@"%team_id%" : ValueOrNull(event.teamID),
|
||||
@"%signing_id%" : ValueOrNull(event.signingID),
|
||||
@"%cdhash%" : ValueOrNull(event.cdhash),
|
||||
@"%machine_id%" : ValueOrNull(config.machineID),
|
||||
@"%hostname%" : ValueOrNull([SNTSystemInfo longHostname]),
|
||||
@"%uuid%" : ValueOrNull([SNTSystemInfo hardwareUUID]),
|
||||
@"%serial%" : ValueOrNull([SNTSystemInfo serialNumber]),
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Everything from `+eventDetailTemplateMappingForEvent:` with the following file access
|
||||
// specific templates.
|
||||
//
|
||||
// %rule_version% - The version of the rule that was violated.
|
||||
// %rule_name% - The name of the rule that was violated.
|
||||
// %accessed_path% - The path accessed by the binary.
|
||||
//
|
||||
+ (NSDictionary *)fileAccessEventDetailTemplateMappingForEvent:(SNTFileAccessEvent *)event {
|
||||
NSMutableDictionary *d = [self eventDetailTemplateMappingForEvent:event].mutableCopy;
|
||||
[d addEntriesFromDictionary:@{
|
||||
@"%rule_version%" : ValueOrNull(event.ruleVersion),
|
||||
@"%rule_name%" : ValueOrNull(event.ruleName),
|
||||
@"%accessed_path%" : ValueOrNull(event.accessedPath),
|
||||
}];
|
||||
return d;
|
||||
}
|
||||
|
||||
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
|
||||
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
|
||||
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
|
||||
// The "format strings" in `templateMapping` will be replaced in the URL, if they are present.
|
||||
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event
|
||||
customURL:(NSString *)url
|
||||
templateMapping:(NSDictionary *)templateMapping {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
NSString *formatStr = url;
|
||||
@@ -166,26 +218,7 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Disabling clang-format. See comment in `eventDetailURLForFileAccessEvent:customURL:`
|
||||
// clang-format off
|
||||
NSDictionary<NSString *, NSString * (^)()> *kvReplacements =
|
||||
[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
// This key is deprecated, use %file_identifier% or %bundle_or_file_identifier%
|
||||
^{ return event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil; },
|
||||
@"%file_sha%",
|
||||
^{ return event.fileSHA256; }, @"%file_identifier%",
|
||||
^{ return event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil; },
|
||||
@"%bundle_or_file_identifier%",
|
||||
^{ return event.executingUser; }, @"%username%",
|
||||
^{ return config.machineID; }, @"%machine_id%",
|
||||
^{ return [SNTSystemInfo longHostname]; }, @"%hostname%",
|
||||
^{ return [SNTSystemInfo hardwareUUID]; }, @"%uuid%",
|
||||
^{ return [SNTSystemInfo serialNumber]; }, @"%serial%",
|
||||
nil];
|
||||
// clang-format on
|
||||
|
||||
formatStr = [SNTBlockMessage replaceFormatString:formatStr withDict:kvReplacements];
|
||||
|
||||
formatStr = [SNTBlockMessage replaceFormatString:formatStr withDict:templateMapping];
|
||||
NSURL *u = [NSURL URLWithString:formatStr];
|
||||
if (!u) {
|
||||
LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
|
||||
@@ -194,55 +227,16 @@
|
||||
return u;
|
||||
}
|
||||
|
||||
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
|
||||
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
|
||||
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
|
||||
// The following "format strings" will be replaced in the URL, if they are present:
|
||||
//
|
||||
// %rule_version% - The version of the rule that was violated.
|
||||
// %rule_name% - The name of the rule that was violated.
|
||||
// %file_identifier% - The SHA-256 of the binary being executed.
|
||||
// %accessed_path% - The path accessed by the binary.
|
||||
// %username% - The executing user's name.
|
||||
// %machine_id% - The configured machine ID for this host.
|
||||
// %hostname% - The machine's FQDN.
|
||||
// %uuid% - The machine's UUID.
|
||||
// %serial% - The machine's serial number.
|
||||
//
|
||||
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url {
|
||||
return [self eventDetailURLForEvent:event
|
||||
customURL:url
|
||||
templateMapping:[self eventDetailTemplateMappingForEvent:event]];
|
||||
}
|
||||
|
||||
+ (NSURL *)eventDetailURLForFileAccessEvent:(SNTFileAccessEvent *)event customURL:(NSString *)url {
|
||||
if (!url.length || [url isEqualToString:@"null"]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
// Clang format goes wild here. If you use the container literal syntax `@{}` with a block value
|
||||
// type, it seems to break the clang format on/off functionality and breaks formatting for the
|
||||
// remainder of the file.
|
||||
// Using `dictionaryWithObjectsAndKeys` and disabling clang format as a workaround.
|
||||
// clang-format off
|
||||
NSDictionary<NSString *, NSString * (^)()> *kvReplacements =
|
||||
[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
^{ return event.ruleVersion; }, @"%rule_version%",
|
||||
^{ return event.ruleName; }, @"%rule_name%",
|
||||
^{ return event.fileSHA256; }, @"%file_identifier%",
|
||||
^{ return event.accessedPath; }, @"%accessed_path%",
|
||||
^{ return event.executingUser; }, @"%username%",
|
||||
^{ return config.machineID; }, @"%machine_id%",
|
||||
^{ return [SNTSystemInfo longHostname]; }, @"%hostname%",
|
||||
^{ return [SNTSystemInfo hardwareUUID]; }, @"%uuid%",
|
||||
^{ return [SNTSystemInfo serialNumber]; }, @"%serial%",
|
||||
nil];
|
||||
// clang-format on
|
||||
|
||||
NSString *formatStr = [SNTBlockMessage replaceFormatString:url withDict:kvReplacements];
|
||||
|
||||
NSURL *u = [NSURL URLWithString:formatStr];
|
||||
if (!u) {
|
||||
LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
|
||||
}
|
||||
|
||||
return u;
|
||||
return [self eventDetailURLForEvent:event
|
||||
customURL:url
|
||||
templateMapping:[self fileAccessEventDetailTemplateMappingForEvent:event]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -39,18 +39,30 @@
|
||||
OCMStub([self.mockSystemInfo serialNumber]).andReturn(@"my_s");
|
||||
}
|
||||
|
||||
- (void)testFormatMessage {
|
||||
NSString *input = @"Testing with somé Ünicode çharacters";
|
||||
NSAttributedString *got = [SNTBlockMessage formatMessage:input];
|
||||
XCTAssertEqualObjects([got string], input);
|
||||
}
|
||||
|
||||
- (void)testEventDetailURLForEvent {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
|
||||
se.fileSHA256 = @"my_fi";
|
||||
se.executingUser = @"my_un";
|
||||
se.fileBundleID = @"s.n.t";
|
||||
se.cdhash = @"abc";
|
||||
se.teamID = @"SNT";
|
||||
se.signingID = @"SNT:s.n.t";
|
||||
|
||||
NSString *url = @"http://"
|
||||
@"localhost?fs=%file_sha%&fi=%file_identifier%&bfi=%bundle_or_file_identifier%&"
|
||||
@"fbid=%file_bundle_id%&ti=%team_id%&si=%signing_id%&ch=%cdhash%&"
|
||||
@"un=%username%&mid=%machine_id%&hn=%hostname%&u=%uuid%&s=%serial%";
|
||||
NSString *wantUrl =
|
||||
@"http://"
|
||||
@"localhost?fs=my_fi&fi=my_fi&bfi=my_fi&bfi=my_fi&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
|
||||
NSString *wantUrl = @"http://"
|
||||
@"localhost?fs=my_fi&fi=my_fi&bfi=my_fi&"
|
||||
@"fbid=s.n.t&ti=SNT&si=SNT:s.n.t&ch=abc&"
|
||||
@"un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
|
||||
|
||||
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
|
||||
|
||||
@@ -58,7 +70,9 @@
|
||||
se.fileBundleHash = @"my_fbh";
|
||||
|
||||
wantUrl = @"http://"
|
||||
@"localhost?fs=my_fbh&fi=my_fi&bfi=my_fbh&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
|
||||
@"localhost?fs=my_fbh&fi=my_fi&bfi=my_fbh&"
|
||||
@"fbid=s.n.t&ti=SNT&si=SNT:s.n.t&ch=abc&"
|
||||
@"un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
|
||||
|
||||
gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
|
||||
|
||||
@@ -74,15 +88,22 @@
|
||||
fae.ruleVersion = @"my_rv";
|
||||
fae.ruleName = @"my_rn";
|
||||
fae.fileSHA256 = @"my_fi";
|
||||
fae.fileBundleID = @"s.n.t";
|
||||
fae.cdhash = @"abc";
|
||||
fae.teamID = @"SNT";
|
||||
fae.signingID = @"SNT:s.n.t";
|
||||
fae.accessedPath = @"my_ap";
|
||||
fae.executingUser = @"my_un";
|
||||
|
||||
NSString *url = @"http://"
|
||||
@"localhost?rv=%rule_version%&rn=%rule_name%&fi=%file_identifier%&ap=%accessed_"
|
||||
@"path%&un=%username%&mid=%machine_id%&hn=%hostname%&u=%uuid%&s=%serial%";
|
||||
NSString *wantUrl =
|
||||
NSString *url =
|
||||
@"http://"
|
||||
@"localhost?rv=my_rv&rn=my_rn&fi=my_fi&ap=my_ap&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
|
||||
@"localhost?rv=%rule_version%&rn=%rule_name%&fi=%file_identifier%&"
|
||||
@"fbid=%file_bundle_id%&ti=%team_id%&si=%signing_id%&ch=%cdhash%&"
|
||||
@"ap=%accessed_path%&un=%username%&mid=%machine_id%&hn=%hostname%&u=%uuid%&s=%serial%";
|
||||
NSString *wantUrl = @"http://"
|
||||
@"localhost?rv=my_rv&rn=my_rn&fi=my_fi&"
|
||||
@"fbid=s.n.t&ti=SNT&si=SNT:s.n.t&ch=abc&"
|
||||
@"ap=my_ap&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
|
||||
|
||||
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:url];
|
||||
|
||||
@@ -92,4 +113,17 @@
|
||||
XCTAssertNil([SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:@"null"]);
|
||||
}
|
||||
|
||||
- (void)testEventDetailURLMissingDetails {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
|
||||
se.fileSHA256 = @"my_fi";
|
||||
|
||||
NSString *url = @"http://localhost?fi=%file_identifier%";
|
||||
NSString *wantUrl = @"http://localhost?fi=my_fi";
|
||||
|
||||
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
|
||||
|
||||
XCTAssertEqualObjects(gotUrl.absoluteString, wantUrl);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
///
|
||||
@interface SNTCachedDecision : NSObject
|
||||
|
||||
- (instancetype)init;
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile;
|
||||
- (instancetype)initWithVnode:(SantaVnode)vnode NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@property SantaVnode vnodeId;
|
||||
@property SNTEventState decision;
|
||||
@@ -38,6 +40,7 @@
|
||||
@property NSArray<MOLCertificate *> *certChain;
|
||||
@property NSString *teamID;
|
||||
@property NSString *signingID;
|
||||
@property NSString *cdhash;
|
||||
@property NSDictionary *entitlements;
|
||||
@property BOOL entitlementsFiltered;
|
||||
|
||||
|
||||
@@ -17,10 +17,18 @@
|
||||
|
||||
@implementation SNTCachedDecision
|
||||
|
||||
- (instancetype)init {
|
||||
return [self initWithVnode:(SantaVnode){}];
|
||||
}
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile {
|
||||
return [self initWithVnode:SantaVnode::VnodeForFile(esFile)];
|
||||
}
|
||||
|
||||
- (instancetype)initWithVnode:(SantaVnode)vnode {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_vnodeId = SantaVnode::VnodeForFile(esFile);
|
||||
_vnodeId = vnode;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ typedef NS_ENUM(NSInteger, SNTAction) {
|
||||
typedef NS_ENUM(NSInteger, SNTRuleType) {
|
||||
SNTRuleTypeUnknown = 0,
|
||||
|
||||
SNTRuleTypeCDHash = 500,
|
||||
SNTRuleTypeBinary = 1000,
|
||||
SNTRuleTypeSigningID = 2000,
|
||||
SNTRuleTypeCertificate = 3000,
|
||||
@@ -84,6 +85,7 @@ typedef NS_ENUM(uint64_t, SNTEventState) {
|
||||
SNTEventStateBlockTeamID = 1ULL << 20,
|
||||
SNTEventStateBlockLongPath = 1ULL << 21,
|
||||
SNTEventStateBlockSigningID = 1ULL << 22,
|
||||
SNTEventStateBlockCDHash = 1ULL << 23,
|
||||
|
||||
// Bits 40-63 store allow decision types
|
||||
SNTEventStateAllowUnknown = 1ULL << 40,
|
||||
@@ -95,6 +97,7 @@ typedef NS_ENUM(uint64_t, SNTEventState) {
|
||||
SNTEventStateAllowPendingTransitive = 1ULL << 46,
|
||||
SNTEventStateAllowTeamID = 1ULL << 47,
|
||||
SNTEventStateAllowSigningID = 1ULL << 48,
|
||||
SNTEventStateAllowCDHash = 1ULL << 49,
|
||||
|
||||
// Block and Allow masks
|
||||
SNTEventStateBlock = 0xFFFFFFULL << 16,
|
||||
@@ -179,6 +182,7 @@ typedef NS_ENUM(NSInteger, SNTRuleCleanup) {
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
enum class FileAccessPolicyDecision {
|
||||
kNoPolicy,
|
||||
kDenied,
|
||||
@@ -187,6 +191,19 @@ enum class FileAccessPolicyDecision {
|
||||
kAllowedReadAccess,
|
||||
kAllowedAuditOnly,
|
||||
};
|
||||
|
||||
enum class StatChangeStep {
|
||||
kNoChange = 0,
|
||||
kMessageCreate,
|
||||
kCodesignValidation,
|
||||
};
|
||||
|
||||
enum class StatResult {
|
||||
kOK = 0,
|
||||
kStatError,
|
||||
kDevnoInodeMismatch,
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
static const char *kSantaDPath =
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
///
|
||||
/// Enable Fail Close mode. Defaults to NO.
|
||||
/// This controls Santa's behavior when a failure occurs, such as an
|
||||
/// inability to read a file. By default, to prevent bugs or misconfiguration
|
||||
/// inability to read a file and as a default response when deadlines
|
||||
/// are about to expire. By default, to prevent bugs or misconfiguration
|
||||
/// from rendering a machine inoperable Santa will fail open and allow
|
||||
/// execution. With this setting enabled, Santa will fail closed if the client
|
||||
/// is in LOCKDOWN mode, offering a higher level of security but with a higher
|
||||
@@ -654,6 +655,12 @@
|
||||
///
|
||||
@property(readonly, nonatomic) NSArray<NSString *> *entitlementsTeamIDFilter;
|
||||
|
||||
///
|
||||
/// List of enabled process annotations.
|
||||
/// This property is not KVO compliant.
|
||||
///
|
||||
@property(readonly, nonatomic) NSArray<NSString *> *enabledProcessAnnotations;
|
||||
|
||||
///
|
||||
/// Retrieve an initialized singleton configurator object using the default file path.
|
||||
///
|
||||
|
||||
@@ -147,6 +147,8 @@ static NSString *const kMetricExportInterval = @"MetricExportInterval";
|
||||
static NSString *const kMetricExportTimeout = @"MetricExportTimeout";
|
||||
static NSString *const kMetricExtraLabels = @"MetricExtraLabels";
|
||||
|
||||
static NSString *const kEnabledProcessAnnotations = @"EnabledProcessAnnotations";
|
||||
|
||||
// The keys managed by a sync server or mobileconfig.
|
||||
static NSString *const kClientModeKey = @"ClientMode";
|
||||
static NSString *const kBlockUSBMountKey = @"BlockUSBMount";
|
||||
@@ -275,6 +277,7 @@ static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
|
||||
kOverrideFileAccessActionKey : string,
|
||||
kEntitlementsPrefixFilterKey : array,
|
||||
kEntitlementsTeamIDFilterKey : array,
|
||||
kEnabledProcessAnnotations : array,
|
||||
};
|
||||
|
||||
_syncStateFilePath = syncStateFilePath;
|
||||
@@ -604,8 +607,7 @@ static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
|
||||
|
||||
- (BOOL)failClosed {
|
||||
NSNumber *n = self.configState[kFailClosedKey];
|
||||
if (n) return [n boolValue];
|
||||
return NO;
|
||||
return [n boolValue] && self.clientMode == SNTClientModeLockdown;
|
||||
}
|
||||
|
||||
- (BOOL)enableTransitiveRules {
|
||||
@@ -1106,6 +1108,16 @@ static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
|
||||
return self.configState[kMetricExtraLabels];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)enabledProcessAnnotations {
|
||||
NSArray<NSString *> *annotations = self.configState[kEnabledProcessAnnotations];
|
||||
for (id annotation in annotations) {
|
||||
if (![annotation isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
return annotations;
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
///
|
||||
@@ -1210,6 +1222,39 @@ static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
|
||||
return [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];
|
||||
}
|
||||
|
||||
- (void)applyOverrides:(NSMutableDictionary *)forcedConfig {
|
||||
// Overrides should only be applied under debug builds.
|
||||
#ifdef DEBUG
|
||||
if ([[[NSProcessInfo processInfo] processName] isEqualToString:@"xctest"] &&
|
||||
![[[NSProcessInfo processInfo] environment] objectForKey:@"ENABLE_CONFIG_OVERRIDES"]) {
|
||||
// By default, config overrides are not applied when running tests to help
|
||||
// mitigate potential issues due to unexpected config values. This behavior
|
||||
// can be overriden if desired by using the env variable: `ENABLE_CONFIG_OVERRIDES`.
|
||||
//
|
||||
// E.g.:
|
||||
// bazel test --test_env=ENABLE_CONFIG_OVERRIDES=1 ...other test args...
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *overrides = [NSDictionary dictionaryWithContentsOfFile:kConfigOverrideFilePath];
|
||||
for (NSString *key in overrides) {
|
||||
id obj = overrides[key];
|
||||
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]] ||
|
||||
([self.forcedConfigKeyTypes[key] isKindOfClass:[NSRegularExpression class]] &&
|
||||
![obj isKindOfClass:[NSString class]])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
forcedConfig[key] = obj;
|
||||
|
||||
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
|
||||
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;
|
||||
forcedConfig[key] = [self expressionForPattern:pattern];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (NSMutableDictionary *)readForcedConfig {
|
||||
NSMutableDictionary *forcedConfig = [NSMutableDictionary dictionary];
|
||||
for (NSString *key in self.forcedConfigKeyTypes) {
|
||||
@@ -1221,22 +1266,9 @@ static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
|
||||
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]] ||
|
||||
([self.forcedConfigKeyTypes[key] isKindOfClass:[NSRegularExpression class]] &&
|
||||
![obj isKindOfClass:[NSString class]])) {
|
||||
continue;
|
||||
}
|
||||
forcedConfig[key] = obj;
|
||||
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
|
||||
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;
|
||||
forcedConfig[key] = [self expressionForPattern:pattern];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
[self applyOverrides:forcedConfig];
|
||||
|
||||
return forcedConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
|
||||
///
|
||||
/// Represents an event stored in the database.
|
||||
///
|
||||
@interface SNTFileAccessEvent : NSObject <NSSecureCoding>
|
||||
@interface SNTFileAccessEvent : SNTStoredEvent <NSSecureCoding>
|
||||
|
||||
///
|
||||
/// The watched path that was accessed
|
||||
@@ -32,57 +32,11 @@
|
||||
@property NSString *ruleVersion;
|
||||
@property NSString *ruleName;
|
||||
|
||||
///
|
||||
/// The SHA256 of the process that accessed the path
|
||||
///
|
||||
@property NSString *fileSHA256;
|
||||
|
||||
///
|
||||
/// The path of the process that accessed the watched path
|
||||
///
|
||||
@property NSString *filePath;
|
||||
|
||||
///
|
||||
/// If the process is part of a bundle, the name of the application
|
||||
///
|
||||
@property NSString *application;
|
||||
|
||||
///
|
||||
/// If the executed file was signed, this is the Team ID if present in the signature information.
|
||||
///
|
||||
@property NSString *teamID;
|
||||
|
||||
///
|
||||
/// If the executed file was signed, this is the Signing ID if present in the signature information.
|
||||
///
|
||||
@property NSString *signingID;
|
||||
|
||||
///
|
||||
/// The user who executed the binary.
|
||||
///
|
||||
@property NSString *executingUser;
|
||||
|
||||
///
|
||||
/// The process ID of the binary being executed.
|
||||
///
|
||||
@property NSNumber *pid;
|
||||
|
||||
///
|
||||
/// The parent process ID of the binary being executed.
|
||||
///
|
||||
@property NSNumber *ppid;
|
||||
|
||||
///
|
||||
/// The name of the parent process.
|
||||
///
|
||||
@property NSString *parentName;
|
||||
|
||||
///
|
||||
/// If the executed file was signed, this is an NSArray of MOLCertificate's
|
||||
/// representing the signing chain.
|
||||
///
|
||||
@property NSArray<MOLCertificate *> *signingChain;
|
||||
|
||||
///
|
||||
/// A string representing the publisher based on the signingChain
|
||||
///
|
||||
|
||||
@@ -48,35 +48,20 @@
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
[super encodeWithCoder:coder];
|
||||
ENCODE(accessedPath);
|
||||
ENCODE(ruleVersion);
|
||||
ENCODE(ruleName);
|
||||
ENCODE(fileSHA256);
|
||||
ENCODE(filePath);
|
||||
ENCODE(application);
|
||||
ENCODE(teamID);
|
||||
ENCODE(teamID);
|
||||
ENCODE(pid);
|
||||
ENCODE(ppid);
|
||||
ENCODE(parentName);
|
||||
ENCODE(signingChain);
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)decoder {
|
||||
self = [super init];
|
||||
self = [super initWithCoder:decoder];
|
||||
if (self) {
|
||||
DECODE(accessedPath, NSString);
|
||||
DECODE(ruleVersion, NSString);
|
||||
DECODE(ruleName, NSString);
|
||||
DECODE(fileSHA256, NSString);
|
||||
DECODE(filePath, NSString);
|
||||
DECODE(application, NSString);
|
||||
DECODE(teamID, NSString);
|
||||
DECODE(teamID, NSString);
|
||||
DECODE(pid, NSNumber);
|
||||
DECODE(ppid, NSNumber);
|
||||
DECODE(parentName, NSString);
|
||||
DECODEARRAY(signingChain, MOLCertificate);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#import <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SantaVnode.h"
|
||||
|
||||
@class MOLCodesignChecker;
|
||||
|
||||
///
|
||||
@@ -220,6 +222,11 @@
|
||||
///
|
||||
- (NSUInteger)fileSize;
|
||||
|
||||
///
|
||||
/// @return The devno/ino pair of the file
|
||||
///
|
||||
- (SantaVnode)vnode;
|
||||
|
||||
///
|
||||
/// @return The underlying file handle.
|
||||
///
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
@property NSString *path;
|
||||
@property NSFileHandle *fileHandle;
|
||||
@property NSUInteger fileSize;
|
||||
@property SantaVnode vnode;
|
||||
@property NSString *fileOwnerHomeDir;
|
||||
@property NSString *sha256Storage;
|
||||
|
||||
@@ -110,6 +111,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
}
|
||||
|
||||
_fileSize = fileStat->st_size;
|
||||
_vnode = (SantaVnode){.fsid = fileStat->st_dev, .fileid = fileStat->st_ino};
|
||||
|
||||
if (_fileSize == 0) return nil;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
#include <CommonCrypto/CommonCrypto.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#include <os/base.h>
|
||||
|
||||
#import "Source/common/SNTSyncConstants.h"
|
||||
@@ -103,6 +104,14 @@ static const NSUInteger kExpectedTeamIDLength = 10;
|
||||
break;
|
||||
}
|
||||
|
||||
case SNTRuleTypeCDHash: {
|
||||
identifier = [[identifier lowercaseString] stringByTrimmingCharactersInSet:nonHex];
|
||||
if (identifier.length != CS_CDHASH_LEN * 2) {
|
||||
return nil;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
@@ -173,6 +182,8 @@ static const NSUInteger kExpectedTeamIDLength = 10;
|
||||
type = SNTRuleTypeTeamID;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeSigningID]) {
|
||||
type = SNTRuleTypeSigningID;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeCDHash]) {
|
||||
type = SNTRuleTypeCDHash;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
51
Source/common/SNTRuleIdentifiers.h
Normal file
51
Source/common/SNTRuleIdentifiers.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/// Copyright 2024 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.
|
||||
|
||||
/**
|
||||
* This file declares two types that are mirrors of each other.
|
||||
*
|
||||
* The C struct serves as a way to group and pass valid rule identifiers around
|
||||
* in order to minimize interface changes needed when new rule types are added
|
||||
* and also alleviate the need to allocate a short lived object.
|
||||
*
|
||||
* The Objective C class is used for an XPC boundary to easily pass rule
|
||||
* identifiers between Santa components.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
struct RuleIdentifiers {
|
||||
NSString *cdhash;
|
||||
NSString *binarySHA256;
|
||||
NSString *signingID;
|
||||
NSString *certificateSHA256;
|
||||
NSString *teamID;
|
||||
};
|
||||
|
||||
@interface SNTRuleIdentifiers : NSObject <NSSecureCoding>
|
||||
@property(readonly) NSString *cdhash;
|
||||
@property(readonly) NSString *binarySHA256;
|
||||
@property(readonly) NSString *signingID;
|
||||
@property(readonly) NSString *certificateSHA256;
|
||||
@property(readonly) NSString *teamID;
|
||||
|
||||
/// Please use `initWithRuleIdentifiers:`
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithRuleIdentifiers:(struct RuleIdentifiers)identifiers
|
||||
NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (struct RuleIdentifiers)toStruct;
|
||||
|
||||
@end
|
||||
73
Source/common/SNTRuleIdentifiers.m
Normal file
73
Source/common/SNTRuleIdentifiers.m
Normal file
@@ -0,0 +1,73 @@
|
||||
/// Copyright 2024 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
|
||||
@implementation SNTRuleIdentifiers
|
||||
|
||||
- (instancetype)initWithRuleIdentifiers:(struct RuleIdentifiers)identifiers {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_cdhash = identifiers.cdhash;
|
||||
_binarySHA256 = identifiers.binarySHA256;
|
||||
_signingID = identifiers.signingID;
|
||||
_certificateSHA256 = identifiers.certificateSHA256;
|
||||
_teamID = identifiers.teamID;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (struct RuleIdentifiers)toStruct {
|
||||
return (struct RuleIdentifiers){.cdhash = self.cdhash,
|
||||
.binarySHA256 = self.binarySHA256,
|
||||
.signingID = self.signingID,
|
||||
.certificateSHA256 = self.certificateSHA256,
|
||||
.teamID = self.teamID};
|
||||
}
|
||||
|
||||
#pragma mark NSSecureCoding
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wobjc-literal-conversion"
|
||||
#define ENCODE(obj, key) \
|
||||
if (obj) [coder encodeObject:obj forKey:key]
|
||||
#define DECODE(cls, key) [decoder decodeObjectOfClass:[cls class] forKey:key]
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)decoder {
|
||||
self = [self init];
|
||||
if (self) {
|
||||
_cdhash = DECODE(NSString, @"cdhash");
|
||||
_binarySHA256 = DECODE(NSString, @"binarySHA256");
|
||||
_signingID = DECODE(NSString, @"signingID");
|
||||
_certificateSHA256 = DECODE(NSString, @"certificateSHA256");
|
||||
_teamID = DECODE(NSString, @"teamID");
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
ENCODE(self.cdhash, @"cdhash");
|
||||
ENCODE(self.binarySHA256, @"binarySHA256");
|
||||
ENCODE(self.signingID, @"signingID");
|
||||
ENCODE(self.certificateSHA256, @"certificateSHA256");
|
||||
ENCODE(self.teamID, @"teamID");
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
||||
@@ -105,6 +105,11 @@
|
||||
///
|
||||
@property NSString *signingID;
|
||||
|
||||
///
|
||||
/// If the executed file was signed, this is the CDHash of the binary.
|
||||
///
|
||||
@property NSString *cdhash;
|
||||
|
||||
///
|
||||
/// The user who executed the binary.
|
||||
///
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
ENCODE(self.signingChain, @"signingChain");
|
||||
ENCODE(self.teamID, @"teamID");
|
||||
ENCODE(self.signingID, @"signingID");
|
||||
ENCODE(self.cdhash, @"cdhash");
|
||||
|
||||
ENCODE(self.executingUser, @"executingUser");
|
||||
ENCODE(self.occurrenceDate, @"occurrenceDate");
|
||||
@@ -97,6 +98,7 @@
|
||||
_signingChain = DECODEARRAY(MOLCertificate, @"signingChain");
|
||||
_teamID = DECODE(NSString, @"teamID");
|
||||
_signingID = DECODE(NSString, @"signingID");
|
||||
_cdhash = DECODE(NSString, @"cdhash");
|
||||
|
||||
_executingUser = DECODE(NSString, @"executingUser");
|
||||
_occurrenceDate = DECODE(NSDate, @"occurrenceDate");
|
||||
|
||||
@@ -44,6 +44,7 @@ extern NSString *const kCompilerRuleCount;
|
||||
extern NSString *const kTransitiveRuleCount;
|
||||
extern NSString *const kTeamIDRuleCount;
|
||||
extern NSString *const kSigningIDRuleCount;
|
||||
extern NSString *const kCDHashRuleCount;
|
||||
extern NSString *const kFullSyncInterval;
|
||||
extern NSString *const kFCMToken;
|
||||
extern NSString *const kFCMFullSyncInterval;
|
||||
@@ -70,12 +71,14 @@ extern NSString *const kDecisionAllowCertificate;
|
||||
extern NSString *const kDecisionAllowScope;
|
||||
extern NSString *const kDecisionAllowTeamID;
|
||||
extern NSString *const kDecisionAllowSigningID;
|
||||
extern NSString *const kDecisionAllowCDHash;
|
||||
extern NSString *const kDecisionBlockUnknown;
|
||||
extern NSString *const kDecisionBlockBinary;
|
||||
extern NSString *const kDecisionBlockCertificate;
|
||||
extern NSString *const kDecisionBlockScope;
|
||||
extern NSString *const kDecisionBlockTeamID;
|
||||
extern NSString *const kDecisionBlockSigningID;
|
||||
extern NSString *const kDecisionBlockCDHash;
|
||||
extern NSString *const kDecisionUnknown;
|
||||
extern NSString *const kDecisionBundleBinary;
|
||||
extern NSString *const kLoggedInUsers;
|
||||
@@ -101,6 +104,7 @@ extern NSString *const kCertValidFrom;
|
||||
extern NSString *const kCertValidUntil;
|
||||
extern NSString *const kTeamID;
|
||||
extern NSString *const kSigningID;
|
||||
extern NSString *const kCDHash;
|
||||
extern NSString *const kQuarantineDataURL;
|
||||
extern NSString *const kQuarantineRefererURL;
|
||||
extern NSString *const kQuarantineTimestamp;
|
||||
@@ -125,6 +129,7 @@ extern NSString *const kRuleTypeBinary;
|
||||
extern NSString *const kRuleTypeCertificate;
|
||||
extern NSString *const kRuleTypeTeamID;
|
||||
extern NSString *const kRuleTypeSigningID;
|
||||
extern NSString *const kRuleTypeCDHash;
|
||||
extern NSString *const kRuleCustomMsg;
|
||||
extern NSString *const kRuleCustomURL;
|
||||
extern NSString *const kCursor;
|
||||
|
||||
@@ -44,6 +44,7 @@ NSString *const kCompilerRuleCount = @"compiler_rule_count";
|
||||
NSString *const kTransitiveRuleCount = @"transitive_rule_count";
|
||||
NSString *const kTeamIDRuleCount = @"teamid_rule_count";
|
||||
NSString *const kSigningIDRuleCount = @"signingid_rule_count";
|
||||
NSString *const kCDHashRuleCount = @"cdhash_rule_count";
|
||||
NSString *const kFullSyncInterval = @"full_sync_interval";
|
||||
NSString *const kFCMToken = @"fcm_token";
|
||||
NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval";
|
||||
@@ -71,12 +72,14 @@ NSString *const kDecisionAllowCertificate = @"ALLOW_CERTIFICATE";
|
||||
NSString *const kDecisionAllowScope = @"ALLOW_SCOPE";
|
||||
NSString *const kDecisionAllowTeamID = @"ALLOW_TEAMID";
|
||||
NSString *const kDecisionAllowSigningID = @"ALLOW_SIGNINGID";
|
||||
NSString *const kDecisionAllowCDHash = @"ALLOW_CDHASH";
|
||||
NSString *const kDecisionBlockUnknown = @"BLOCK_UNKNOWN";
|
||||
NSString *const kDecisionBlockBinary = @"BLOCK_BINARY";
|
||||
NSString *const kDecisionBlockCertificate = @"BLOCK_CERTIFICATE";
|
||||
NSString *const kDecisionBlockScope = @"BLOCK_SCOPE";
|
||||
NSString *const kDecisionBlockTeamID = @"BLOCK_TEAMID";
|
||||
NSString *const kDecisionBlockSigningID = @"BLOCK_SIGNINGID";
|
||||
NSString *const kDecisionBlockCDHash = @"BLOCK_CDHASH";
|
||||
NSString *const kDecisionUnknown = @"UNKNOWN";
|
||||
NSString *const kDecisionBundleBinary = @"BUNDLE_BINARY";
|
||||
NSString *const kLoggedInUsers = @"logged_in_users";
|
||||
@@ -102,6 +105,7 @@ NSString *const kCertValidFrom = @"valid_from";
|
||||
NSString *const kCertValidUntil = @"valid_until";
|
||||
NSString *const kTeamID = @"team_id";
|
||||
NSString *const kSigningID = @"signing_id";
|
||||
NSString *const kCDHash = @"cdhash";
|
||||
NSString *const kQuarantineDataURL = @"quarantine_data_url";
|
||||
NSString *const kQuarantineRefererURL = @"quarantine_referer_url";
|
||||
NSString *const kQuarantineTimestamp = @"quarantine_timestamp";
|
||||
@@ -126,6 +130,7 @@ NSString *const kRuleTypeBinary = @"BINARY";
|
||||
NSString *const kRuleTypeCertificate = @"CERTIFICATE";
|
||||
NSString *const kRuleTypeTeamID = @"TEAMID";
|
||||
NSString *const kRuleTypeSigningID = @"SIGNINGID";
|
||||
NSString *const kRuleTypeCDHash = @"CDHASH";
|
||||
NSString *const kRuleCustomMsg = @"custom_msg";
|
||||
NSString *const kRuleCustomURL = @"custom_url";
|
||||
NSString *const kCursor = @"cursor";
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/common/SNTXPCUnprivilegedControlInterface.h"
|
||||
|
||||
///
|
||||
@@ -32,11 +33,8 @@
|
||||
reply:(void (^)(NSError *error))reply;
|
||||
- (void)databaseEventsPending:(void (^)(NSArray *events))reply;
|
||||
- (void)databaseRemoveEventsWithIDs:(NSArray *)ids;
|
||||
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTRule *))reply;
|
||||
- (void)databaseRuleForIdentifiers:(SNTRuleIdentifiers *)identifiers
|
||||
reply:(void (^)(SNTRule *))reply;
|
||||
- (void)retrieveAllRules:(void (^)(NSArray<SNTRule *> *rules, NSError *error))reply;
|
||||
|
||||
///
|
||||
|
||||
@@ -34,8 +34,7 @@ NSString *const kBundleID = @"com.google.santa.daemon";
|
||||
#else
|
||||
MOLCodesignChecker *cs = [[MOLCodesignChecker alloc] initWithSelf];
|
||||
// "teamid.com.google.santa.daemon.xpc"
|
||||
NSString *t = cs.signingInformation[@"teamid"];
|
||||
return [NSString stringWithFormat:@"%@.%@.xpc", t, kBundleID];
|
||||
return [NSString stringWithFormat:@"%@.%@.xpc", cs.teamID, kBundleID];
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,23 @@
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
|
||||
@class SNTRule;
|
||||
@class SNTStoredEvent;
|
||||
@class MOLXPCConnection;
|
||||
|
||||
struct RuleCounts {
|
||||
int64_t binary;
|
||||
int64_t certificate;
|
||||
int64_t compiler;
|
||||
int64_t transitive;
|
||||
int64_t teamID;
|
||||
int64_t signingID;
|
||||
int64_t cdhash;
|
||||
};
|
||||
|
||||
///
|
||||
/// Protocol implemented by santad and utilized by santactl (unprivileged operations)
|
||||
///
|
||||
@@ -36,8 +47,7 @@
|
||||
///
|
||||
/// Database ops
|
||||
///
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID, int64_t signingID))reply;
|
||||
- (void)databaseRuleCounts:(void (^)(struct RuleCounts ruleCounts))reply;
|
||||
- (void)databaseEventCount:(void (^)(int64_t count))reply;
|
||||
- (void)staticRuleCount:(void (^)(int64_t count))reply;
|
||||
|
||||
@@ -47,17 +57,10 @@
|
||||
|
||||
///
|
||||
/// @param filePath A Path to the file, can be nil.
|
||||
/// @param fileSHA256 The pre-calculated SHA256 hash for the file, can be nil. If nil the hash will
|
||||
/// be calculated by this method from the filePath.
|
||||
/// @param certificateSHA256 A SHA256 hash of the signing certificate, can be nil.
|
||||
/// @note If fileInfo and signingCertificate are both passed in, the most specific rule will be
|
||||
/// returned. Binary rules take precedence over cert rules.
|
||||
/// @param identifiers The various identifiers to be used when making a decision.
|
||||
///
|
||||
- (void)decisionForFilePath:(NSString *)filePath
|
||||
fileSHA256:(NSString *)fileSHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
identifiers:(SNTRuleIdentifiers *)identifiers
|
||||
reply:(void (^)(SNTEventState))reply;
|
||||
|
||||
///
|
||||
|
||||
@@ -320,8 +320,8 @@ class SantaCache {
|
||||
Lock a bucket. Spins until the lock is acquired.
|
||||
*/
|
||||
inline void lock(struct bucket *bucket) const {
|
||||
while (OSAtomicTestAndSet(7, (volatile uint8_t *)&bucket->head))
|
||||
;
|
||||
while (OSAtomicTestAndSet(7, (volatile uint8_t *)&bucket->head)) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ syntax = "proto3";
|
||||
|
||||
import "google/protobuf/any.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "Source/santad/ProcessTree/process_tree.proto";
|
||||
|
||||
option objc_class_prefix = "SNTPB";
|
||||
|
||||
@@ -173,6 +174,8 @@ message ProcessInfo {
|
||||
|
||||
// Time the process was started
|
||||
optional google.protobuf.Timestamp start_time = 17;
|
||||
|
||||
optional process_tree.Annotations annotations = 18;
|
||||
}
|
||||
|
||||
// Light variant of ProcessInfo message to help minimize on-disk/on-wire sizes
|
||||
@@ -202,6 +205,8 @@ message ProcessInfoLight {
|
||||
|
||||
// File information for the executable backing this process
|
||||
optional FileInfoLight executable = 10;
|
||||
|
||||
optional process_tree.Annotations annotations = 11;
|
||||
}
|
||||
|
||||
// Certificate information
|
||||
@@ -284,6 +289,7 @@ message Execution {
|
||||
REASON_LONG_PATH = 9;
|
||||
REASON_NOT_RUNNING = 10;
|
||||
REASON_SIGNING_ID = 11;
|
||||
REASON_CDHASH = 12;
|
||||
}
|
||||
optional Reason reason = 10;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22689"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -18,18 +18,18 @@
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Santa Blocked Execution" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" visibleAtLaunch="NO" animationBehavior="none" id="9Bq-yh-54f" customClass="SNTMessageWindow">
|
||||
<windowStyleMask key="styleMask" utility="YES"/>
|
||||
<window title="Santa Blocked Execution" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" visibleAtLaunch="NO" animationBehavior="none" titlebarAppearsTransparent="YES" titleVisibility="hidden" id="9Bq-yh-54f" customClass="SNTMessageWindow">
|
||||
<windowStyleMask key="styleMask" titled="YES" utility="YES"/>
|
||||
<rect key="contentRect" x="167" y="107" width="540" height="479"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1916" height="1099"/>
|
||||
<view key="contentView" id="Iwq-Lx-rLv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="540" height="462"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kiB-jZ-69S">
|
||||
<rect key="frame" x="16" y="434" width="37" height="32"/>
|
||||
<rect key="frame" x="16" y="434" width="37" height="23"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="push" title="Hidden Button" alternateTitle="This button exists so neither of the other two buttons is pre-selected when the dialog opens." bezelStyle="rounded" alignment="center" borderStyle="border" focusRingType="none" transparent="YES" imageScaling="proportionallyDown" inset="2" id="XGa-Sl-F4t">
|
||||
<buttonCell key="cell" type="roundTextured" title="Hidden Button" alternateTitle="This button exists so neither of the other two buttons is pre-selected when the dialog opens." bezelStyle="texturedRounded" alignment="center" borderStyle="border" focusRingType="none" transparent="YES" imageScaling="proportionallyDown" inset="2" id="XGa-Sl-F4t">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
@@ -128,10 +128,6 @@
|
||||
</textField>
|
||||
<button toolTip="Show code signing certificate chain" translatesAutoresizingMaskIntoConstraints="NO" id="cJf-k6-OxS" userLabel="Publisher Certs Button">
|
||||
<rect key="frame" x="62" y="235" width="15" height="15"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="15" id="QTm-Iv-m5p"/>
|
||||
<constraint firstAttribute="height" constant="15" id="YwG-0s-jop"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="bevel" bezelStyle="regularSquare" image="NSInfo" imagePosition="overlaps" alignment="center" refusesFirstResponder="YES" imageScaling="proportionallyDown" inset="2" id="R72-Qy-Xbb">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -139,6 +135,10 @@
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="15" id="QTm-Iv-m5p"/>
|
||||
<constraint firstAttribute="height" constant="15" id="YwG-0s-jop"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="showCertInfo:" target="-2" id="dB0-a3-X31"/>
|
||||
<binding destination="-2" name="hidden" keyPath="self.publisherInfo" id="fFR-f3-Oiw">
|
||||
@@ -241,16 +241,17 @@
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="7ua-5a-uSd">
|
||||
<rect key="frame" x="147" y="30" width="126" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" priority="900" constant="112" id="Pec-Pa-4aZ"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Open..." bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" priority="900" constant="112" id="Pec-Pa-4aZ"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="openEventDetails:" target="-2" id="VhL-ql-rCV"/>
|
||||
<outlet property="nextKeyView" destination="BbV-3h-mmL" id="Xkz-va-iGc"/>
|
||||
@@ -272,23 +273,19 @@ DQ
|
||||
</textField>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="5D8-GP-a4l">
|
||||
<rect key="frame" x="110" y="80" width="319" height="29"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="25" id="KvD-X6-CsO"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="check" title="Prevent future notifications for this application for a day" bezelStyle="regularSquare" imagePosition="left" alignment="center" inset="2" id="R5Y-Uc-rEP">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="25" id="KvD-X6-CsO"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.silenceFutureNotifications" id="tEb-2A-sht"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="BbV-3h-mmL" userLabel="Dismiss Button">
|
||||
<rect key="frame" x="271" y="28" width="124" height="34"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="110" id="6Uh-Bd-N64"/>
|
||||
<constraint firstAttribute="height" constant="22" id="GH6-nw-6rD"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Ignore" bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="XR6-Xa-gP4">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -296,6 +293,10 @@ DQ
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="110" id="6Uh-Bd-N64"/>
|
||||
<constraint firstAttribute="height" constant="22" id="GH6-nw-6rD"/>
|
||||
</constraints>
|
||||
<accessibility description="Dismiss Dialog"/>
|
||||
<connections>
|
||||
<action selector="closeWindow:" target="-2" id="qQq-gh-8lw"/>
|
||||
|
||||
@@ -92,6 +92,10 @@
|
||||
NSString *eventDetailText = [[SNTConfigurator configurator] eventDetailText];
|
||||
if (eventDetailText) {
|
||||
[self.openEventButton setTitle:eventDetailText];
|
||||
// Require the button keyEquivalent set to be CMD + Return
|
||||
[self.openEventButton setKeyEquivalent:@"\r"]; // Return Key
|
||||
[self.openEventButton
|
||||
setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; // Command Key
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,8 @@ struct SNTFileAccessMessageWindowView: View {
|
||||
|
||||
Text(customText ?? "Open Event...").frame(maxWidth:.infinity)
|
||||
})
|
||||
.buttonStyle(.borderedProminent)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
Button(action: dismissButton, label: {
|
||||
Text("Dismiss").frame(maxWidth:.infinity)
|
||||
|
||||
@@ -226,6 +226,15 @@
|
||||
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
se.signingChain = cs.certificates;
|
||||
se.cdhash = cs.cdhash;
|
||||
se.teamID = cs.teamID;
|
||||
if (cs.signingID) {
|
||||
if (cs.teamID) {
|
||||
se.signingID = [NSString stringWithFormat:@"%@:%@", cs.teamID, cs.signingID];
|
||||
} else if (cs.platformBinary) {
|
||||
se.signingID = [NSString stringWithFormat:@"platform:%@", cs.signingID];
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
relatedEvents[se.fileSHA256] = se;
|
||||
|
||||
@@ -68,6 +68,7 @@ objc_library(
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTStrengthify",
|
||||
"//Source/common:SNTSystemInfo",
|
||||
@@ -116,6 +117,7 @@ santa_unit_test(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTXPCBundleServiceInterface",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTXPCBundleServiceInterface.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
@@ -45,6 +46,7 @@ static NSString *const kSigningChain = @"Signing Chain";
|
||||
static NSString *const kUniversalSigningChain = @"Universal Signing Chain";
|
||||
static NSString *const kTeamID = @"Team ID";
|
||||
static NSString *const kSigningID = @"Signing ID";
|
||||
static NSString *const kCDHash = @"CDHash";
|
||||
|
||||
// signing chain keys
|
||||
static NSString *const kCommonName = @"Common Name";
|
||||
@@ -123,6 +125,7 @@ typedef id (^SNTAttributeBlock)(SNTCommandFileInfo *, SNTFileInfo *);
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadAgent;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock teamID;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock signingID;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock cdhash;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock type;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock pageZero;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock codeSigned;
|
||||
@@ -201,8 +204,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
+ (NSArray<NSString *> *)fileInfoKeys {
|
||||
return @[
|
||||
kPath, kSHA256, kSHA1, kBundleName, kBundleVersion, kBundleVersionStr, kDownloadReferrerURL,
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kType, kPageZero,
|
||||
kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kCDHash, kType,
|
||||
kPageZero, kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
|
||||
];
|
||||
}
|
||||
|
||||
@@ -236,6 +239,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
kUniversalSigningChain : self.universalSigningChain,
|
||||
kTeamID : self.teamID,
|
||||
kSigningID : self.signingID,
|
||||
kCDHash : self.cdhash,
|
||||
};
|
||||
|
||||
_printQueue = dispatch_queue_create("com.google.santactl.print_queue", DISPATCH_QUEUE_SERIAL);
|
||||
@@ -376,33 +380,34 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
NSError *err;
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err];
|
||||
|
||||
NSString *teamID =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
|
||||
NSString *identifier =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
NSString *cdhash = csc.cdhash;
|
||||
NSString *teamID = csc.teamID;
|
||||
NSString *identifier = csc.signingID;
|
||||
|
||||
NSString *signingID;
|
||||
if (identifier) {
|
||||
if (teamID) {
|
||||
signingID = [NSString stringWithFormat:@"%@:%@", teamID, identifier];
|
||||
} else {
|
||||
id platformID =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
|
||||
if ([platformID isKindOfClass:[NSNumber class]] && [platformID intValue] != 0) {
|
||||
signingID = [NSString stringWithFormat:@"platform:%@", identifier];
|
||||
}
|
||||
} else if (csc.platformBinary) {
|
||||
signingID = [NSString stringWithFormat:@"platform:%@", identifier];
|
||||
}
|
||||
}
|
||||
|
||||
[[cmd.daemonConn remoteObjectProxy] decisionForFilePath:fileInfo.path
|
||||
fileSHA256:fileInfo.SHA256
|
||||
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTEventState s) {
|
||||
state = s;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
struct RuleIdentifiers identifiers = {
|
||||
.cdhash = cdhash,
|
||||
.binarySHA256 = fileInfo.SHA256,
|
||||
.signingID = signingID,
|
||||
.certificateSHA256 = err ? nil : csc.leafCertificate.SHA256,
|
||||
.teamID = teamID,
|
||||
};
|
||||
|
||||
[[cmd.daemonConn remoteObjectProxy]
|
||||
decisionForFilePath:fileInfo.path
|
||||
identifiers:[[SNTRuleIdentifiers alloc] initWithRuleIdentifiers:identifiers]
|
||||
reply:^(SNTEventState s) {
|
||||
state = s;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
cmd.daemonUnavailable = YES;
|
||||
return kCommunicationErrorMsg;
|
||||
@@ -420,6 +425,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
case SNTEventStateAllowSigningID:
|
||||
case SNTEventStateBlockSigningID: [output appendString:@" (SigningID)"]; break;
|
||||
case SNTEventStateAllowCDHash:
|
||||
case SNTEventStateBlockCDHash: [output appendString:@" (CDHash)"]; break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
|
||||
@@ -508,14 +515,30 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
- (SNTAttributeBlock)teamID {
|
||||
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
|
||||
return [csc.signingInformation valueForKey:@"teamid"];
|
||||
return csc.teamID;
|
||||
};
|
||||
}
|
||||
|
||||
- (SNTAttributeBlock)signingID {
|
||||
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
|
||||
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
|
||||
NSString *identifier = csc.signingID;
|
||||
NSString *teamID = csc.teamID;
|
||||
if (!identifier) return nil;
|
||||
if (teamID) {
|
||||
return [NSString stringWithFormat:@"%@:%@", teamID, identifier];
|
||||
} else if (csc.platformBinary) {
|
||||
return [NSString stringWithFormat:@"platform:%@", identifier];
|
||||
}
|
||||
return nil;
|
||||
};
|
||||
}
|
||||
|
||||
- (SNTAttributeBlock)cdhash {
|
||||
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
|
||||
return csc.cdhash;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ REGISTER_COMMAND_NAME(@"printlog")
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
JsonPrintOptions options;
|
||||
options.always_print_enums_as_ints = false;
|
||||
options.always_print_primitive_fields = true;
|
||||
options.always_print_fields_with_no_presence = true;
|
||||
options.preserve_proto_field_names = true;
|
||||
options.add_whitespace = true;
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Kernel/kern/cs_blobs.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
@@ -22,6 +24,7 @@
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/Commands/SNTCommandRule.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
@@ -44,58 +47,67 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
return (@"Usage: santactl rule [options]\n"
|
||||
@" One of:\n"
|
||||
@" --allow: add to allow\n"
|
||||
@" --block: add to block\n"
|
||||
@" --silent-block: add to silent block\n"
|
||||
@" --compiler: allow and mark as a compiler\n"
|
||||
@" --remove: remove existing rule\n"
|
||||
@" --check: check for an existing rule\n"
|
||||
@" --import {path}: import rules from a JSON file\n"
|
||||
@" --export {path}: export rules to a JSON file\n"
|
||||
@"\n"
|
||||
@" One of:\n"
|
||||
@" --path {path}: path of binary/bundle to add/remove.\n"
|
||||
@" Will add the hash of the file currently at that path.\n"
|
||||
@" Does not work with --check. Use the fileinfo verb to check.\n"
|
||||
@" the rule state of a file.\n"
|
||||
@" --identifier {sha256|teamID|signingID}: identifier to add/remove/check\n"
|
||||
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
|
||||
@"\n"
|
||||
@" Optionally:\n"
|
||||
@" --teamid: add or check a team ID rule instead of binary\n"
|
||||
@" --signingid: add or check a signing ID rule instead of binary (see notes)\n"
|
||||
@" --certificate: add or check a certificate sha256 rule instead of binary\n"
|
||||
return (
|
||||
@"Usage: santactl rule [options]\n"
|
||||
@" One of:\n"
|
||||
@" --allow: add to allow\n"
|
||||
@" --block: add to block\n"
|
||||
@" --silent-block: add to silent block\n"
|
||||
@" --compiler: allow and mark as a compiler\n"
|
||||
@" --remove: remove existing rule\n"
|
||||
@" --check: check for an existing rule\n"
|
||||
@" --import {path}: import rules from a JSON file\n"
|
||||
@" --export {path}: export rules to a JSON file\n"
|
||||
@"\n"
|
||||
@" One of:\n"
|
||||
@" --path {path}: path of binary/bundle to add/remove.\n"
|
||||
@" Will add an appropriate rule for the file currently at that path.\n"
|
||||
@" Defaults to a SHA-256 rule unless overridden with another flag.\n"
|
||||
@" Does not work with --check. Use the fileinfo verb to check.\n"
|
||||
@" the rule state of a file.\n"
|
||||
@" --identifier {sha256|teamID|signingID|cdhash}: identifier to add/remove/check\n"
|
||||
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
|
||||
@"\n"
|
||||
@" Optionally:\n"
|
||||
@" --teamid: add or check a team ID rule instead of binary\n"
|
||||
@" --signingid: add or check a signing ID rule instead of binary (see notes)\n"
|
||||
@" --certificate: add or check a certificate sha256 rule instead of binary\n"
|
||||
@" --cdhash: add or check a cdhash rule instead of binary\n"
|
||||
#ifdef DEBUG
|
||||
@" --force: allow manual changes even when SyncBaseUrl is set\n"
|
||||
@" --force: allow manual changes even when SyncBaseUrl is set\n"
|
||||
#endif
|
||||
@" --message {message}: custom message\n"
|
||||
@"\n"
|
||||
@" Notes:\n"
|
||||
@" The format of `identifier` when adding/checking a `signingid` rule is:\n"
|
||||
@"\n"
|
||||
@" `TeamID:SigningID`\n"
|
||||
@"\n"
|
||||
@" Because signing IDs are controlled by the binary author, this ensures\n"
|
||||
@" that the signing ID is properly scoped to a developer. For the special\n"
|
||||
@" case of platform binaries, `TeamID` should be replaced with the string\n"
|
||||
@" \"platform\" (e.g. `platform:SigningID`). This allows for rules\n"
|
||||
@" targeting Apple-signed binaries that do not have a team ID.\n"
|
||||
@"\n"
|
||||
@" Importing / Exporting Rules:\n"
|
||||
@" If santa is not configured to use a sync server one can export\n"
|
||||
@" & import its non-static rules to and from JSON files using the \n"
|
||||
@" --export/--import flags. These files have the following form:\n"
|
||||
@"\n"
|
||||
@" {\"rules\": [{rule-dictionaries}]}\n"
|
||||
@" e.g. {\"rules\": [\n"
|
||||
@" {\"policy\": \"BLOCKLIST\",\n"
|
||||
@" \"identifier\": "
|
||||
@"\"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6\"\n"
|
||||
@" \"custom_url\" : \"\",\n"
|
||||
@" \"custom_msg\": \"/bin/ls block for demo\"}\n"
|
||||
@" ]}\n");
|
||||
@" --message {message}: custom message\n"
|
||||
@" --clean: when importing rules via JSON clear all non-transitive rules before importing\n"
|
||||
@" --clean-all: when importing rules via JSON clear all rules before importing\n"
|
||||
@"\n"
|
||||
@" Notes:\n"
|
||||
@" The format of `identifier` when adding/checking a `signingid` rule is:\n"
|
||||
@"\n"
|
||||
@" `TeamID:SigningID`\n"
|
||||
@"\n"
|
||||
@" Because signing IDs are controlled by the binary author, this ensures\n"
|
||||
@" that the signing ID is properly scoped to a developer. For the special\n"
|
||||
@" case of platform binaries, `TeamID` should be replaced with the string\n"
|
||||
@" \"platform\" (e.g. `platform:SigningID`). This allows for rules\n"
|
||||
@" targeting Apple-signed binaries that do not have a team ID.\n"
|
||||
@"\n"
|
||||
@" Importing / Exporting Rules:\n"
|
||||
@" If santa is not configured to use a sync server one can export\n"
|
||||
@" & import its non-static rules to and from JSON files using the \n"
|
||||
@" --export/--import flags. These files have the following form:\n"
|
||||
@"\n"
|
||||
@" {\"rules\": [{rule-dictionaries}]}\n"
|
||||
@" e.g. {\"rules\": [\n"
|
||||
@" {\"policy\": \"BLOCKLIST\",\n"
|
||||
@" \"identifier\": "
|
||||
@"\"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6\"\n"
|
||||
@" \"custom_url\" : \"\",\n"
|
||||
@" \"custom_msg\": \"/bin/ls block for demo\"}\n"
|
||||
@" ]}\n"
|
||||
@"\n"
|
||||
@" By default rules are not cleared when importing. To clear the\n"
|
||||
@" database you must use either --clean or --clean-all\n"
|
||||
@"\n");
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
@@ -119,6 +131,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
NSString *path;
|
||||
NSString *jsonFilePath;
|
||||
BOOL check = NO;
|
||||
SNTRuleCleanup cleanupType = SNTRuleCleanupNone;
|
||||
BOOL importRules = NO;
|
||||
BOOL exportRules = NO;
|
||||
|
||||
@@ -147,6 +160,8 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
newRule.type = SNTRuleTypeTeamID;
|
||||
} else if ([arg caseInsensitiveCompare:@"--signingid"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeSigningID;
|
||||
} else if ([arg caseInsensitiveCompare:@"--cdhash"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeCDHash;
|
||||
} else if ([arg caseInsensitiveCompare:@"--path"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--path requires an argument"];
|
||||
@@ -180,6 +195,10 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
[self printErrorUsageAndExit:@"--import requires an argument"];
|
||||
}
|
||||
jsonFilePath = arguments[i];
|
||||
} else if ([arg caseInsensitiveCompare:@"--clean"] == NSOrderedSame) {
|
||||
cleanupType = SNTRuleCleanupNonTransitive;
|
||||
} else if ([arg caseInsensitiveCompare:@"--clean-all"] == NSOrderedSame) {
|
||||
cleanupType = SNTRuleCleanupAll;
|
||||
} else if ([arg caseInsensitiveCompare:@"--export"] == NSOrderedSame) {
|
||||
if (importRules) {
|
||||
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
|
||||
@@ -198,12 +217,27 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
}
|
||||
|
||||
if (!importRules && cleanupType != SNTRuleCleanupNone) {
|
||||
switch (cleanupType) {
|
||||
case SNTRuleCleanupNonTransitive:
|
||||
[self printErrorUsageAndExit:@"--clean can only be used with --import"];
|
||||
break;
|
||||
case SNTRuleCleanupAll:
|
||||
[self printErrorUsageAndExit:@"--clean-all can only be used with --import"];
|
||||
break;
|
||||
default:
|
||||
// This is a programming error.
|
||||
LOGE(@"Unexpected SNTRuleCleanupType %ld", cleanupType);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonFilePath.length > 0) {
|
||||
if (importRules) {
|
||||
if (newRule.identifier != nil || path != nil || check) {
|
||||
[self printErrorUsageAndExit:@"--import can only be used by itself"];
|
||||
}
|
||||
[self importJSONFile:jsonFilePath];
|
||||
[self importJSONFile:jsonFilePath with:cleanupType];
|
||||
} else if (exportRules) {
|
||||
if (newRule.identifier != nil || path != nil || check) {
|
||||
[self printErrorUsageAndExit:@"--export can only be used by itself"];
|
||||
@@ -224,17 +258,36 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
} else if (newRule.type == SNTRuleTypeCertificate) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.identifier = cs.leafCertificate.SHA256;
|
||||
} else if (newRule.type == SNTRuleTypeTeamID || newRule.type == SNTRuleTypeSigningID) {
|
||||
// noop
|
||||
} else if (newRule.type == SNTRuleTypeCDHash) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.identifier = cs.cdhash;
|
||||
} else if (newRule.type == SNTRuleTypeTeamID) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.identifier = cs.teamID;
|
||||
} else if (newRule.type == SNTRuleTypeSigningID) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
if (cs.teamID.length) {
|
||||
newRule.identifier = [NSString stringWithFormat:@"%@:%@", cs.teamID, cs.signingID];
|
||||
} else if (cs.platformBinary) {
|
||||
newRule.identifier = [NSString stringWithFormat:@"platform:%@", cs.signingID];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) {
|
||||
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate ||
|
||||
newRule.type == SNTRuleTypeCDHash) {
|
||||
NSCharacterSet *nonHex =
|
||||
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
|
||||
if ([[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length !=
|
||||
64) {
|
||||
NSUInteger length =
|
||||
[[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length;
|
||||
|
||||
if ((newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) &&
|
||||
length != CC_SHA256_DIGEST_LENGTH * 2) {
|
||||
[self printErrorUsageAndExit:@"BINARY or CERTIFICATE rules require a valid SHA-256"];
|
||||
} else if (newRule.type == SNTRuleTypeCDHash && length != CS_CDHASH_LEN * 2) {
|
||||
[self printErrorUsageAndExit:
|
||||
[NSString stringWithFormat:@"CDHASH rules require a valid hex string of length %d",
|
||||
CS_CDHASH_LEN * 2]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +299,8 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
if (newRule.state == SNTRuleStateUnknown) {
|
||||
[self printErrorUsageAndExit:@"No state specified"];
|
||||
} else if (!newRule.identifier) {
|
||||
[self printErrorUsageAndExit:@"Either SHA-256, team ID, or path to file must be specified"];
|
||||
[self printErrorUsageAndExit:
|
||||
@"A valid SHA-256, CDHash, Signing ID, team ID, or path to file must be specified"];
|
||||
}
|
||||
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
@@ -261,7 +315,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
} else {
|
||||
NSString *ruleType;
|
||||
switch (newRule.type) {
|
||||
case SNTRuleTypeCertificate:
|
||||
case SNTRuleTypeCertificate: ruleType = @"Certificate SHA-256"; break;
|
||||
case SNTRuleTypeBinary: {
|
||||
ruleType = @"SHA-256";
|
||||
break;
|
||||
@@ -270,6 +324,14 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
ruleType = @"Team ID";
|
||||
break;
|
||||
}
|
||||
case SNTRuleTypeSigningID: {
|
||||
ruleType = @"Signing ID";
|
||||
break;
|
||||
}
|
||||
case SNTRuleTypeCDHash: {
|
||||
ruleType = @"CDHash";
|
||||
break;
|
||||
}
|
||||
default: ruleType = @"(Unknown type)";
|
||||
}
|
||||
if (newRule.state == SNTRuleStateRemove) {
|
||||
@@ -323,6 +385,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeUnknown: [output appendString:@"Unknown"]; break;
|
||||
case SNTRuleTypeCDHash: [output appendString:@"CDHash"]; break;
|
||||
case SNTRuleTypeBinary: [output appendString:@"Binary"]; break;
|
||||
case SNTRuleTypeSigningID: [output appendString:@"SigningID"]; break;
|
||||
case SNTRuleTypeCertificate: [output appendString:@"Certificate"]; break;
|
||||
@@ -365,26 +428,27 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
|
||||
- (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)daemonConn {
|
||||
id<SNTDaemonControlXPC> rop = [daemonConn synchronousRemoteObjectProxy];
|
||||
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil;
|
||||
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil;
|
||||
NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil;
|
||||
NSString *signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil;
|
||||
__block NSString *output;
|
||||
|
||||
[rop databaseRuleForBinarySHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTRule *r) {
|
||||
output = [SNTCommandRule stringifyRule:r
|
||||
withColor:(isatty(STDOUT_FILENO) == 1)];
|
||||
}];
|
||||
struct RuleIdentifiers identifiers = {
|
||||
.cdhash = (rule.type == SNTRuleTypeCDHash) ? rule.identifier : nil,
|
||||
.binarySHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil,
|
||||
.certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil,
|
||||
.teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil,
|
||||
.signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil,
|
||||
};
|
||||
|
||||
[rop databaseRuleForIdentifiers:[[SNTRuleIdentifiers alloc] initWithRuleIdentifiers:identifiers]
|
||||
reply:^(SNTRule *r) {
|
||||
output = [SNTCommandRule stringifyRule:r
|
||||
withColor:(isatty(STDOUT_FILENO) == 1)];
|
||||
}];
|
||||
|
||||
printf("%s\n", output.UTF8String);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
- (void)importJSONFile:(NSString *)jsonFilePath {
|
||||
- (void)importJSONFile:(NSString *)jsonFilePath with:(SNTRuleCleanup)cleanupType {
|
||||
// If the file exists parse it and then add the rules one at a time.
|
||||
NSError *error;
|
||||
NSData *data = [NSData dataWithContentsOfFile:jsonFilePath options:0 error:&error];
|
||||
@@ -421,7 +485,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleAddRules:parsedRules
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
ruleCleanup:cleanupType
|
||||
reply:^(NSError *error) {
|
||||
if (error) {
|
||||
printf("Failed to modify rules: %s",
|
||||
|
||||
@@ -91,22 +91,20 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}];
|
||||
|
||||
// Database counts
|
||||
__block int64_t eventCount = -1;
|
||||
__block int64_t binaryRuleCount = -1;
|
||||
__block int64_t certRuleCount = -1;
|
||||
__block int64_t teamIDRuleCount = -1;
|
||||
__block int64_t signingIDRuleCount = -1;
|
||||
__block int64_t compilerRuleCount = -1;
|
||||
__block int64_t transitiveRuleCount = -1;
|
||||
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID, int64_t signingID) {
|
||||
binaryRuleCount = binary;
|
||||
certRuleCount = certificate;
|
||||
teamIDRuleCount = teamID;
|
||||
signingIDRuleCount = signingID;
|
||||
compilerRuleCount = compiler;
|
||||
transitiveRuleCount = transitive;
|
||||
__block struct RuleCounts ruleCounts = {
|
||||
.binary = -1,
|
||||
.certificate = -1,
|
||||
.compiler = -1,
|
||||
.transitive = -1,
|
||||
.teamID = -1,
|
||||
.signingID = -1,
|
||||
.cdhash = -1,
|
||||
};
|
||||
[rop databaseRuleCounts:^(struct RuleCounts counts) {
|
||||
ruleCounts = counts;
|
||||
}];
|
||||
|
||||
__block int64_t eventCount = -1;
|
||||
[rop databaseEventCount:^(int64_t count) {
|
||||
eventCount = count;
|
||||
}];
|
||||
@@ -212,12 +210,13 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"on_start_usb_options" : StartupOptionToString(configurator.onStartUSBOptions),
|
||||
},
|
||||
@"database" : @{
|
||||
@"binary_rules" : @(binaryRuleCount),
|
||||
@"certificate_rules" : @(certRuleCount),
|
||||
@"teamid_rules" : @(teamIDRuleCount),
|
||||
@"signingid_rules" : @(signingIDRuleCount),
|
||||
@"compiler_rules" : @(compilerRuleCount),
|
||||
@"transitive_rules" : @(transitiveRuleCount),
|
||||
@"binary_rules" : @(ruleCounts.binary),
|
||||
@"certificate_rules" : @(ruleCounts.certificate),
|
||||
@"teamid_rules" : @(ruleCounts.teamID),
|
||||
@"signingid_rules" : @(ruleCounts.signingID),
|
||||
@"cdhash_rules" : @(ruleCounts.cdhash),
|
||||
@"compiler_rules" : @(ruleCounts.compiler),
|
||||
@"transitive_rules" : @(ruleCounts.transitive),
|
||||
@"events_pending_upload" : @(eventCount),
|
||||
},
|
||||
@"static_rules" : @{
|
||||
@@ -284,12 +283,13 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %lld\n", "Non-root cache count", nonRootCacheCount);
|
||||
|
||||
printf(">>> Database Info\n");
|
||||
printf(" %-25s | %lld\n", "Binary Rules", binaryRuleCount);
|
||||
printf(" %-25s | %lld\n", "Certificate Rules", certRuleCount);
|
||||
printf(" %-25s | %lld\n", "TeamID Rules", teamIDRuleCount);
|
||||
printf(" %-25s | %lld\n", "SigningID Rules", signingIDRuleCount);
|
||||
printf(" %-25s | %lld\n", "Compiler Rules", compilerRuleCount);
|
||||
printf(" %-25s | %lld\n", "Transitive Rules", transitiveRuleCount);
|
||||
printf(" %-25s | %lld\n", "Binary Rules", ruleCounts.binary);
|
||||
printf(" %-25s | %lld\n", "Certificate Rules", ruleCounts.certificate);
|
||||
printf(" %-25s | %lld\n", "TeamID Rules", ruleCounts.teamID);
|
||||
printf(" %-25s | %lld\n", "SigningID Rules", ruleCounts.signingID);
|
||||
printf(" %-25s | %lld\n", "CDHash Rules", ruleCounts.cdhash);
|
||||
printf(" %-25s | %lld\n", "Compiler Rules", ruleCounts.compiler);
|
||||
printf(" %-25s | %lld\n", "Transitive Rules", ruleCounts.transitive);
|
||||
printf(" %-25s | %lld\n", "Events Pending Upload", eventCount);
|
||||
|
||||
if ([SNTConfigurator configurator].staticRules.count) {
|
||||
|
||||
@@ -33,6 +33,7 @@ objc_library(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
@@ -192,13 +193,10 @@ objc_library(
|
||||
|
||||
objc_library(
|
||||
name = "SNTPolicyProcessor",
|
||||
srcs = [
|
||||
"DataLayer/SNTDatabaseTable.h",
|
||||
"DataLayer/SNTRuleTable.h",
|
||||
"SNTPolicyProcessor.m",
|
||||
],
|
||||
srcs = ["SNTPolicyProcessor.mm"],
|
||||
hdrs = ["SNTPolicyProcessor.h"],
|
||||
deps = [
|
||||
":SNTRuleTable",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -206,10 +204,24 @@ objc_library(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"@FMDB",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
"@MOLXPCConnection",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTPolicyProcessorTest",
|
||||
srcs = ["SNTPolicyProcessorTest.mm"],
|
||||
deps = [
|
||||
":SNTPolicyProcessor",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:TestUtils",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -286,12 +298,27 @@ objc_library(
|
||||
":SNTEndpointSecurityClientBase",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SystemResources",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTEndpointSecurityTreeAwareClient",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityTreeAwareClient.mm"],
|
||||
hdrs = ["EventProviders/SNTEndpointSecurityTreeAwareClient.h"],
|
||||
deps = [
|
||||
":EndpointSecurityAPI",
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":SNTEndpointSecurityClient",
|
||||
"//Source/santad/ProcessTree:SNTEndpointSecurityAdapter",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTEndpointSecurityRecorder",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityRecorder.mm"],
|
||||
@@ -305,13 +332,14 @@ objc_library(
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":SNTCompilerController",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
":SNTEndpointSecurityTreeAwareClient",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -445,6 +473,8 @@ objc_library(
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/santad/ProcessTree:SNTEndpointSecurityAdapter",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -453,6 +483,7 @@ objc_library(
|
||||
hdrs = ["EventProviders/EndpointSecurity/EnrichedTypes.h"],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -627,6 +658,8 @@ objc_library(
|
||||
deps = [
|
||||
":EndpointSecurityClient",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -639,6 +672,9 @@ objc_library(
|
||||
name = "EndpointSecurityAPI",
|
||||
srcs = ["EventProviders/EndpointSecurity/EndpointSecurityAPI.mm"],
|
||||
hdrs = ["EventProviders/EndpointSecurity/EndpointSecurityAPI.h"],
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityClient",
|
||||
":EndpointSecurityMessage",
|
||||
@@ -667,6 +703,7 @@ objc_library(
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTStrengthify",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
@@ -682,6 +719,7 @@ objc_library(
|
||||
srcs = ["Metrics.mm"],
|
||||
hdrs = ["Metrics.h"],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
":SNTApplicationCoreMetrics",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
@@ -723,6 +761,7 @@ objc_library(
|
||||
"//Source/common:SNTXPCNotifierInterface",
|
||||
"//Source/common:SNTXPCSyncServiceInterface",
|
||||
"//Source/common:Unit",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
@@ -754,6 +793,8 @@ objc_library(
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"//Source/common:SNTXPCUnprivilegedControlInterface",
|
||||
"//Source/common:Unit",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
"//Source/santad/ProcessTree/annotations:originator",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
@@ -876,18 +917,17 @@ santa_unit_test(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"@FMDB",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SantadTest",
|
||||
srcs = ["SantadTest.mm"],
|
||||
data = [
|
||||
"//Source/santad/testdata:binaryrules_testdata",
|
||||
],
|
||||
minimum_os_version = "11.0",
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
@@ -896,6 +936,9 @@ santa_unit_test(
|
||||
sdk_frameworks = [
|
||||
"DiskArbitration",
|
||||
],
|
||||
structured_resources = [
|
||||
"//Source/santad/testdata:binaryrules_testdata",
|
||||
],
|
||||
tags = ["exclusive"],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
@@ -904,6 +947,7 @@ santa_unit_test(
|
||||
":SNTDatabaseController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityAuthorizer",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SantadDeps",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -986,7 +1030,7 @@ santa_unit_test(
|
||||
santa_unit_test(
|
||||
name = "EndpointSecuritySerializerProtobufTest",
|
||||
srcs = ["Logs/EndpointSecurity/Serializers/ProtobufTest.mm"],
|
||||
data = [
|
||||
structured_resources = [
|
||||
"//Source/santad/testdata:protobuf_json_testdata",
|
||||
],
|
||||
deps = [
|
||||
@@ -1141,7 +1185,10 @@ santa_unit_test(
|
||||
name = "MetricsTest",
|
||||
srcs = ["MetricsTest.mm"],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
@@ -1161,6 +1208,7 @@ santa_unit_test(
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
],
|
||||
@@ -1181,7 +1229,9 @@ santa_unit_test(
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityClient",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SystemResources",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
@@ -1209,6 +1259,7 @@ santa_unit_test(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"//Source/common:TestUtils",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
@@ -1325,6 +1376,7 @@ santa_unit_test(
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityDeviceManager",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -1383,10 +1435,13 @@ test_suite(
|
||||
":SNTEndpointSecurityTamperResistanceTest",
|
||||
":SNTEventTableTest",
|
||||
":SNTExecutionControllerTest",
|
||||
":SNTPolicyProcessorTest",
|
||||
":SNTRuleTableTest",
|
||||
":SantadTest",
|
||||
":WatchItemsTest",
|
||||
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool_test",
|
||||
"//Source/santad/ProcessTree:process_tree_test",
|
||||
"//Source/santad/ProcessTree/annotations:originator_test",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/santad/DataLayer/SNTDatabaseTable.h"
|
||||
|
||||
@class SNTCachedDecision;
|
||||
@@ -29,46 +30,49 @@
|
||||
///
|
||||
/// @return Number of rules in the database
|
||||
///
|
||||
- (NSUInteger)ruleCount;
|
||||
- (int64_t)ruleCount;
|
||||
|
||||
///
|
||||
/// @return Number of binary rules in the database
|
||||
///
|
||||
- (NSUInteger)binaryRuleCount;
|
||||
- (int64_t)binaryRuleCount;
|
||||
|
||||
///
|
||||
/// @return Number of compiler rules in the database
|
||||
///
|
||||
- (NSUInteger)compilerRuleCount;
|
||||
- (int64_t)compilerRuleCount;
|
||||
|
||||
///
|
||||
/// @return Number of transitive rules in the database
|
||||
///
|
||||
- (NSUInteger)transitiveRuleCount;
|
||||
- (int64_t)transitiveRuleCount;
|
||||
|
||||
///
|
||||
/// @return Number of certificate rules in the database
|
||||
///
|
||||
- (NSUInteger)certificateRuleCount;
|
||||
- (int64_t)certificateRuleCount;
|
||||
|
||||
///
|
||||
/// @return Number of team ID rules in the database
|
||||
///
|
||||
- (NSUInteger)teamIDRuleCount;
|
||||
- (int64_t)teamIDRuleCount;
|
||||
|
||||
///
|
||||
/// @return Number of signing ID rules in the database
|
||||
///
|
||||
- (NSUInteger)signingIDRuleCount;
|
||||
- (int64_t)signingIDRuleCount;
|
||||
|
||||
///
|
||||
/// @return Rule for binary, signingID, certificate or teamID (in that order).
|
||||
/// @return Number of cdhash rules in the database
|
||||
///
|
||||
- (int64_t)cdhashRuleCount;
|
||||
|
||||
///
|
||||
/// @return Rule for given identifiers.
|
||||
/// Currently: binary, signingID, certificate or teamID (in that order).
|
||||
/// The first matching rule found is returned.
|
||||
///
|
||||
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
|
||||
signingID:(NSString *)signingID
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID;
|
||||
- (SNTRule *)ruleForIdentifiers:(struct RuleIdentifiers)identifiers;
|
||||
|
||||
///
|
||||
/// Add an array of rules to the database. The rules will be added within a transaction and the
|
||||
|
||||
@@ -29,7 +29,7 @@ static const uint32_t kRuleTableCurrentVersion = 7;
|
||||
|
||||
// TODO(nguyenphillip): this should be configurable.
|
||||
// How many rules must be in database before we start trying to remove transitive rules.
|
||||
static const NSUInteger kTransitiveRuleCullingThreshold = 500000;
|
||||
static const int64_t kTransitiveRuleCullingThreshold = 500000;
|
||||
// Consider transitive rules out of date if they haven't been used in six months.
|
||||
static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
|
||||
@@ -263,7 +263,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
|
||||
#pragma mark Entry Counts
|
||||
|
||||
- (NSUInteger)ruleCount {
|
||||
- (int64_t)ruleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules"];
|
||||
@@ -271,23 +271,23 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)ruleCountForRuleType:(SNTRuleType)ruleType {
|
||||
__block NSUInteger count = 0;
|
||||
- (int64_t)ruleCountForRuleType:(SNTRuleType)ruleType {
|
||||
__block int64_t count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=?", @(ruleType)];
|
||||
}];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)binaryRuleCount {
|
||||
- (int64_t)binaryRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeBinary];
|
||||
}
|
||||
|
||||
- (NSUInteger)certificateRuleCount {
|
||||
- (int64_t)certificateRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeCertificate];
|
||||
}
|
||||
|
||||
- (NSUInteger)compilerRuleCount {
|
||||
- (int64_t)compilerRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count =
|
||||
@@ -296,7 +296,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)transitiveRuleCount {
|
||||
- (int64_t)transitiveRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count =
|
||||
@@ -305,14 +305,18 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)teamIDRuleCount {
|
||||
- (int64_t)teamIDRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeTeamID];
|
||||
}
|
||||
|
||||
- (NSUInteger)signingIDRuleCount {
|
||||
- (int64_t)signingIDRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeSigningID];
|
||||
}
|
||||
|
||||
- (int64_t)cdhashRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeCDHash];
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs {
|
||||
SNTRule *r = [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
|
||||
state:[rs intForColumn:@"state"]
|
||||
@@ -323,10 +327,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
|
||||
signingID:(NSString *)signingID
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID {
|
||||
- (SNTRule *)ruleForIdentifiers:(struct RuleIdentifiers)identifiers {
|
||||
__block SNTRule *rule;
|
||||
|
||||
// Look for a static rule that matches.
|
||||
@@ -334,22 +335,27 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
if (staticRules.count) {
|
||||
// IMPORTANT: The order static rules are checked here should be the same
|
||||
// order as given by the SQL query for the rules database.
|
||||
rule = staticRules[binarySHA256];
|
||||
rule = staticRules[identifiers.cdhash];
|
||||
if (rule.type == SNTRuleTypeCDHash) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[identifiers.binarySHA256];
|
||||
if (rule.type == SNTRuleTypeBinary) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[signingID];
|
||||
rule = staticRules[identifiers.signingID];
|
||||
if (rule.type == SNTRuleTypeSigningID) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[certificateSHA256];
|
||||
rule = staticRules[identifiers.certificateSHA256];
|
||||
if (rule.type == SNTRuleTypeCertificate) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[teamID];
|
||||
rule = staticRules[identifiers.teamID];
|
||||
if (rule.type == SNTRuleTypeTeamID) {
|
||||
return rule;
|
||||
}
|
||||
@@ -360,9 +366,9 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
// NOTE: This code is written with the intention that the binary rule is searched for first
|
||||
// as Santa is designed to go with the most-specific rule possible.
|
||||
//
|
||||
// The intended order of precedence is Binaries > Signing IDs > Certificates > Team IDs.
|
||||
// The intended order of precedence is CDHash > Binaries > Signing IDs > Certificates > Team IDs.
|
||||
//
|
||||
// As such the query should have "ORDER BY type DESC" before the LIMIT, to ensure that is the
|
||||
// As such the query should have "ORDER BY type ASC" before the LIMIT, to ensure that is the
|
||||
// case. However, in all tested versions of SQLite that ORDER BY clause is unnecessary: the query
|
||||
// is performed 'as written' by doing separate lookups in the index and the later lookups are if
|
||||
// the first returns a result. That behavior can be checked here: http://sqlfiddle.com/#!5/cdc42/1
|
||||
@@ -375,12 +381,15 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
// There is a test for this in SNTRuleTableTests in case SQLite behavior changes in the future.
|
||||
//
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
FMResultSet *rs = [db executeQuery:@"SELECT * FROM rules WHERE "
|
||||
@" (identifier=? and type=1000) "
|
||||
@"OR (identifier=? AND type=2000) "
|
||||
@"OR (identifier=? AND type=3000) "
|
||||
@"OR (identifier=? AND type=4000) LIMIT 1",
|
||||
binarySHA256, signingID, certificateSHA256, teamID];
|
||||
FMResultSet *rs =
|
||||
[db executeQuery:@"SELECT * FROM rules WHERE "
|
||||
@" (identifier=? AND type=500) "
|
||||
@"OR (identifier=? AND type=1000) "
|
||||
@"OR (identifier=? AND type=2000) "
|
||||
@"OR (identifier=? AND type=3000) "
|
||||
@"OR (identifier=? AND type=4000) LIMIT 1",
|
||||
identifiers.cdhash, identifiers.binarySHA256, identifiers.signingID,
|
||||
identifiers.certificateSHA256, identifiers.teamID];
|
||||
if ([rs next]) {
|
||||
rule = [self ruleFromResultSet:rs];
|
||||
}
|
||||
@@ -389,8 +398,8 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
|
||||
// Allow binaries signed by the "Software Signing" cert used to sign launchd
|
||||
// if no existing rule has matched.
|
||||
if (!rule && [certificateSHA256 isEqual:self.launchdCSInfo.leafCertificate.SHA256]) {
|
||||
rule = [[SNTRule alloc] initWithIdentifier:certificateSHA256
|
||||
if (!rule && [identifiers.certificateSHA256 isEqual:self.launchdCSInfo.leafCertificate.SHA256]) {
|
||||
rule = [[SNTRule alloc] initWithIdentifier:identifiers.certificateSHA256
|
||||
state:SNTRuleStateAllow
|
||||
type:SNTRuleTypeCertificate
|
||||
customMsg:nil
|
||||
@@ -454,25 +463,59 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
}
|
||||
|
||||
- (BOOL)addedRulesShouldFlushDecisionCache:(NSArray *)rules {
|
||||
// Check for non-plain-allowlist rules first before querying the database.
|
||||
uint64_t nonAllowRuleCount = 0;
|
||||
|
||||
for (SNTRule *rule in rules) {
|
||||
if (rule.state != SNTRuleStateAllow) return YES;
|
||||
// If the rule is a remove rule, act conservatively and flush the cache.
|
||||
// This is to make sure cached rules of different precedence rules do not
|
||||
// impact final decision.
|
||||
if (rule.state == SNTRuleStateRemove) {
|
||||
return YES;
|
||||
}
|
||||
if (rule.state != SNTRuleStateAllow) {
|
||||
nonAllowRuleCount++;
|
||||
|
||||
// Just flush if we more than 1000 block rules.
|
||||
if (nonAllowRuleCount >= 1000) return YES;
|
||||
}
|
||||
}
|
||||
|
||||
// If still here, then all rules in the array are allowlist rules. So now we look for allowlist
|
||||
// rules where there is a previously existing allowlist compiler rule for the same identifier.
|
||||
// If so we find such a rule, then cache should be flushed.
|
||||
// Check newly synced rules for any blocking rules. If any are found, check
|
||||
// in the db to see if they already exist. If they're not found or were
|
||||
// previously allow rules flush the cache.
|
||||
//
|
||||
// If all rules in the array are allowlist rules, look for allowlist rules
|
||||
// where there is a previously existing allowlist compiler rule for the same
|
||||
// identifier. If so we find such a rule, then cache should be flushed.
|
||||
__block BOOL flushDecisionCache = NO;
|
||||
|
||||
[self inTransaction:^(FMDatabase *db, BOOL *rollback) {
|
||||
for (SNTRule *rule in rules) {
|
||||
// Allowlist certificate rules are ignored
|
||||
if (rule.type == SNTRuleTypeCertificate) continue;
|
||||
// If the rule is a block rule, silent block rule, or a compiler rule check if it already
|
||||
// exists in the database.
|
||||
//
|
||||
// If it does not then flush the cache. To ensure that the new rule is honored.
|
||||
if ((rule.state != SNTRuleStateAllow)) {
|
||||
if ([db longForQuery:
|
||||
@"SELECT COUNT(*) FROM rules WHERE identifier=? AND type=? AND state=? LIMIT 1",
|
||||
rule.identifier, @(rule.type), @(rule.state)] == 0) {
|
||||
flushDecisionCache = YES;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// At this point we know the rule is an allowlist rule. Check if it's
|
||||
// overriding a compiler rule.
|
||||
|
||||
if ([db longForQuery:
|
||||
@"SELECT COUNT(*) FROM rules WHERE identifier=? AND type=? AND state=? LIMIT 1",
|
||||
rule.identifier, @(SNTRuleTypeBinary), @(SNTRuleStateAllowCompiler)] > 0) {
|
||||
flushDecisionCache = YES;
|
||||
break;
|
||||
// Skip certificate and TeamID rules as they cannot be compiler rules.
|
||||
if (rule.type == SNTRuleTypeCertificate || rule.type == SNTRuleTypeTeamID) continue;
|
||||
|
||||
if ([db longForQuery:@"SELECT COUNT(*) FROM rules WHERE identifier=? AND type IN (?, ?, ?)"
|
||||
@" AND state=? LIMIT 1",
|
||||
rule.identifier, @(SNTRuleTypeCDHash), @(SNTRuleTypeBinary),
|
||||
@(SNTRuleTypeSigningID), @(SNTRuleStateAllowCompiler)] > 0) {
|
||||
flushDecisionCache = YES;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
@@ -540,7 +583,6 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
*error = [NSError errorWithDomain:@"com.google.santad.ruletable" code:code userInfo:d];
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark Querying
|
||||
|
||||
// Retrieve all rules from the Database
|
||||
|
||||
@@ -14,15 +14,19 @@
|
||||
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
|
||||
/// This test case actually tests SNTRuleTable and SNTRule
|
||||
@interface SNTRuleTableTest : XCTestCase
|
||||
@property SNTRuleTable *sut;
|
||||
@property FMDatabaseQueue *dbq;
|
||||
@property id mockConfigurator;
|
||||
@end
|
||||
|
||||
@implementation SNTRuleTableTest
|
||||
@@ -32,6 +36,13 @@
|
||||
|
||||
self.dbq = [[FMDatabaseQueue alloc] init];
|
||||
self.sut = [[SNTRuleTable alloc] initWithDatabaseQueue:self.dbq];
|
||||
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockConfigurator stopMocking];
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleTeamIDRule {
|
||||
@@ -56,6 +67,15 @@
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleCDHashRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a";
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeCDHash;
|
||||
r.customMsg = @"A cdhash rule";
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleBinaryRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670";
|
||||
@@ -173,20 +193,20 @@
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.binarySHA256 =
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary);
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.binarySHA256 =
|
||||
@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2",
|
||||
}];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
@@ -196,20 +216,20 @@
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:nil];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.certificateSHA256 =
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate);
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562"
|
||||
teamID:nil];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.certificateSHA256 =
|
||||
@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562",
|
||||
}];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
@@ -218,19 +238,17 @@
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
SNTRule *r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID);
|
||||
XCTAssertEqual([self.sut teamIDRuleCount], 1);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@"nonexistentTeamID"];
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.teamID = @"nonexistentTeamID",
|
||||
}];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
@@ -244,79 +262,141 @@
|
||||
|
||||
XCTAssertEqual([self.sut signingIDRuleCount], 2);
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
SNTRule *r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.signingID = @"ABCDEFGHIJ:signingID",
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:@"platform:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.signingID = @"platform:signingID",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"platform:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil signingID:@"nonexistent" certificateSHA256:nil teamID:nil];
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.signingID = @"nonexistent",
|
||||
}];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchCDHashRule {
|
||||
[self.sut
|
||||
addRules:@[ [self _exampleBinaryRule], [self _exampleTeamIDRule], [self _exampleCDHashRule] ]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
XCTAssertEqual([self.sut cdhashRuleCount], 1);
|
||||
|
||||
SNTRule *r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a",
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCDHash);
|
||||
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"nonexistent",
|
||||
}];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchRuleOrdering {
|
||||
NSError *err;
|
||||
[self.sut addRules:@[
|
||||
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
[self _exampleCertRule],
|
||||
[self _exampleBinaryRule],
|
||||
[self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO],
|
||||
[self _exampleCDHashRule],
|
||||
]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
error:&err];
|
||||
XCTAssertNil(err);
|
||||
|
||||
// This test verifies that the implicit rule ordering we've been abusing is still working.
|
||||
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
|
||||
// See the comment in SNTRuleTable#ruleForIdentifiers:
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a",
|
||||
.binarySHA256 =
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
.signingID = @"ABCDEFGHIJ:signingID",
|
||||
.certificateSHA256 =
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCDHash, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"unknown",
|
||||
.binarySHA256 =
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
.signingID = @"ABCDEFGHIJ:signingID",
|
||||
.certificateSHA256 =
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:@"unknowncert"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"unknown",
|
||||
.binarySHA256 =
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
.signingID = @"ABCDEFGHIJ:signingID",
|
||||
.certificateSHA256 = @"unknown",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"unknown",
|
||||
.binarySHA256 = @"unknown",
|
||||
.signingID = @"unknown",
|
||||
.certificateSHA256 =
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"unknown",
|
||||
.binarySHA256 = @"unknown",
|
||||
.signingID = @"ABCDEFGHIJ:signingID",
|
||||
.certificateSHA256 = @"unknown",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID, @"Implicit rule ordering failed (SigningID)");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"unknown",
|
||||
.binarySHA256 = @"unknown",
|
||||
.signingID = @"unknown",
|
||||
.certificateSHA256 = @"unknown",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID, @"Implicit rule ordering failed (TeamID)");
|
||||
@@ -342,18 +422,95 @@
|
||||
|
||||
- (void)testRetrieveAllRulesWithMultipleRules {
|
||||
[self.sut addRules:@[
|
||||
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
[self _exampleCertRule],
|
||||
[self _exampleBinaryRule],
|
||||
[self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO],
|
||||
[self _exampleCDHashRule],
|
||||
]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
NSArray<SNTRule *> *rules = [self.sut retrieveAllRules];
|
||||
XCTAssertEqual(rules.count, 4);
|
||||
XCTAssertEqual(rules.count, 5);
|
||||
XCTAssertEqualObjects(rules[0], [self _exampleCertRule]);
|
||||
XCTAssertEqualObjects(rules[1], [self _exampleBinaryRule]);
|
||||
XCTAssertEqualObjects(rules[2], [self _exampleTeamIDRule]);
|
||||
XCTAssertEqualObjects(rules[3], [self _exampleSigningIDRuleIsPlatform:NO]);
|
||||
XCTAssertEqualObjects(rules[4], [self _exampleCDHashRule]);
|
||||
}
|
||||
|
||||
- (void)testAddedRulesShouldFlushDecisionCacheWithNewBlockRule {
|
||||
// Ensure that a brand new block rule flushes the decision cache.
|
||||
NSError *error;
|
||||
SNTRule *r = [self _exampleBinaryRule];
|
||||
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
|
||||
XCTAssertNil(error);
|
||||
XCTAssertEqual(self.sut.ruleCount, 1);
|
||||
XCTAssertEqual(self.sut.binaryRuleCount, 1);
|
||||
|
||||
// Change the identifer so that the hash of a block rule is not found in the
|
||||
// db.
|
||||
r.identifier = @"bfff7d3f6c389ebf7a76a666c484d42ea447834901bc29141439ae7c7b96ff09";
|
||||
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
|
||||
}
|
||||
|
||||
// Ensure that a brand new block rule flushes the decision cache.
|
||||
- (void)testAddedRulesShouldFlushDecisionCacheWithOldBlockRule {
|
||||
NSError *error;
|
||||
SNTRule *r = [self _exampleBinaryRule];
|
||||
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
|
||||
XCTAssertNil(error);
|
||||
XCTAssertEqual(self.sut.ruleCount, 1);
|
||||
XCTAssertEqual(self.sut.binaryRuleCount, 1);
|
||||
XCTAssertEqual(NO, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
|
||||
}
|
||||
|
||||
// Ensure that a larger number of blocks flushes the decision cache.
|
||||
- (void)testAddedRulesShouldFlushDecisionCacheWithLargeNumberOfBlocks {
|
||||
NSError *error;
|
||||
SNTRule *r = [self _exampleBinaryRule];
|
||||
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
|
||||
XCTAssertNil(error);
|
||||
XCTAssertEqual(self.sut.ruleCount, 1);
|
||||
XCTAssertEqual(self.sut.binaryRuleCount, 1);
|
||||
NSMutableArray<SNTRule *> *newRules = [NSMutableArray array];
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
newRules[i] = r;
|
||||
}
|
||||
|
||||
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:newRules]);
|
||||
}
|
||||
|
||||
// Ensure that an allow rule that overrides a compiler rule flushes the
|
||||
// decision cache.
|
||||
- (void)testAddedRulesShouldFlushDecisionCacheWithCompilerRule {
|
||||
NSError *error;
|
||||
SNTRule *r = [self _exampleBinaryRule];
|
||||
r.type = SNTRuleTypeBinary;
|
||||
r.state = SNTRuleStateAllowCompiler;
|
||||
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
|
||||
XCTAssertNil(error);
|
||||
XCTAssertEqual(self.sut.ruleCount, 1);
|
||||
XCTAssertEqual(self.sut.binaryRuleCount, 1);
|
||||
// make the rule an allow rule
|
||||
r.state = SNTRuleStateAllow;
|
||||
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
|
||||
}
|
||||
|
||||
// Ensure that an Remove rule targeting an allow rule causes a flush of the cache.
|
||||
- (void)testAddedRulesShouldFlushDecisionCacheWithRemoveRule {
|
||||
NSError *error;
|
||||
SNTRule *r = [self _exampleBinaryRule];
|
||||
r.type = SNTRuleTypeBinary;
|
||||
r.state = SNTRuleStateAllow;
|
||||
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
|
||||
XCTAssertNil(error);
|
||||
XCTAssertEqual(self.sut.ruleCount, 1);
|
||||
XCTAssertEqual(self.sut.binaryRuleCount, 1);
|
||||
|
||||
r.state = SNTRuleStateRemove;
|
||||
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <variant>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.pb.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
@@ -71,25 +72,30 @@ class EnrichedProcess {
|
||||
: effective_user_(std::nullopt),
|
||||
effective_group_(std::nullopt),
|
||||
real_user_(std::nullopt),
|
||||
real_group_(std::nullopt) {}
|
||||
real_group_(std::nullopt),
|
||||
annotations_(std::nullopt) {}
|
||||
|
||||
EnrichedProcess(std::optional<std::shared_ptr<std::string>> &&effective_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&effective_group,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_group,
|
||||
EnrichedFile &&executable)
|
||||
EnrichedProcess(
|
||||
std::optional<std::shared_ptr<std::string>> &&effective_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&effective_group,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_group,
|
||||
EnrichedFile &&executable,
|
||||
std::optional<santa::pb::v1::process_tree::Annotations> &&annotations)
|
||||
: effective_user_(std::move(effective_user)),
|
||||
effective_group_(std::move(effective_group)),
|
||||
real_user_(std::move(real_user)),
|
||||
real_group_(std::move(real_group)),
|
||||
executable_(std::move(executable)) {}
|
||||
executable_(std::move(executable)),
|
||||
annotations_(std::move(annotations)) {}
|
||||
|
||||
EnrichedProcess(EnrichedProcess &&other)
|
||||
: effective_user_(std::move(other.effective_user_)),
|
||||
effective_group_(std::move(other.effective_group_)),
|
||||
real_user_(std::move(other.real_user_)),
|
||||
real_group_(std::move(other.real_group_)),
|
||||
executable_(std::move(other.executable_)) {}
|
||||
executable_(std::move(other.executable_)),
|
||||
annotations_(std::move(other.annotations_)) {}
|
||||
|
||||
// Note: Move assignment could be safely implemented but not currently needed
|
||||
EnrichedProcess &operator=(EnrichedProcess &&other) = delete;
|
||||
@@ -110,6 +116,10 @@ class EnrichedProcess {
|
||||
return real_group_;
|
||||
}
|
||||
const EnrichedFile &executable() const { return executable_; }
|
||||
const std::optional<santa::pb::v1::process_tree::Annotations> &annotations()
|
||||
const {
|
||||
return annotations_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<std::shared_ptr<std::string>> effective_user_;
|
||||
@@ -117,6 +127,7 @@ class EnrichedProcess {
|
||||
std::optional<std::shared_ptr<std::string>> real_user_;
|
||||
std::optional<std::shared_ptr<std::string>> real_group_;
|
||||
EnrichedFile executable_;
|
||||
std::optional<santa::pb::v1::process_tree::Annotations> annotations_;
|
||||
};
|
||||
|
||||
class EnrichedEventType {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
@@ -32,7 +33,7 @@ enum class EnrichOptions {
|
||||
|
||||
class Enricher {
|
||||
public:
|
||||
Enricher();
|
||||
Enricher(std::shared_ptr<process_tree::ProcessTree> pt = nullptr);
|
||||
virtual ~Enricher() = default;
|
||||
virtual std::unique_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual EnrichedProcess Enrich(
|
||||
@@ -51,6 +52,7 @@ class Enricher {
|
||||
username_cache_;
|
||||
SantaCache<gid_t, std::optional<std::shared_ptr<std::string>>>
|
||||
groupname_cache_;
|
||||
std::shared_ptr<process_tree::ProcessTree> process_tree_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
@@ -25,10 +25,14 @@
|
||||
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#include "Source/santad/ProcessTree/process_tree_macos.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Enricher::Enricher() : username_cache_(256), groupname_cache_(256) {}
|
||||
Enricher::Enricher(std::shared_ptr<::santa::santad::process_tree::ProcessTree> pt)
|
||||
: username_cache_(256), groupname_cache_(256), process_tree_(std::move(pt)) {}
|
||||
|
||||
std::unique_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
// TODO(mlw): Consider potential design patterns that could help reduce memory usage under load
|
||||
@@ -89,7 +93,10 @@ EnrichedProcess Enricher::Enrich(const es_process_t &es_proc, EnrichOptions opti
|
||||
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));
|
||||
Enrich(*es_proc.executable, options),
|
||||
process_tree_ ? process_tree_->ExportAnnotations(
|
||||
process_tree::PidFromAuditToken(es_proc.audit_token))
|
||||
: std::nullopt);
|
||||
}
|
||||
|
||||
EnrichedFile Enricher::Enrich(const es_file_t &es_file, EnrichOptions options) {
|
||||
|
||||
@@ -20,9 +20,13 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EndpointSecurityAPI;
|
||||
class MessagePeer;
|
||||
|
||||
class Message {
|
||||
public:
|
||||
@@ -37,17 +41,36 @@ class Message {
|
||||
Message(const Message& other);
|
||||
Message& operator=(const Message& other) = delete;
|
||||
|
||||
void SetProcessToken(process_tree::ProcessToken tok);
|
||||
|
||||
// Operators to access underlying es_message_t
|
||||
const es_message_t* operator->() const { return es_msg_; }
|
||||
const es_message_t& operator*() const { return *es_msg_; }
|
||||
|
||||
// Helper to get the API associated with this message.
|
||||
// Used for things like es_exec_arg_count.
|
||||
// We should ideally rework this to somehow present these functions as methods
|
||||
// on the Message, however this would be a bit of a bigger lift.
|
||||
std::shared_ptr<EndpointSecurityAPI> ESAPI() const { return esapi_; }
|
||||
|
||||
std::string ParentProcessName() const;
|
||||
|
||||
void UpdateStatState(enum StatChangeStep step) const;
|
||||
|
||||
inline StatChangeStep StatChangeStep() const { return stat_change_step_; }
|
||||
inline StatResult StatResult() const { return stat_result_; }
|
||||
|
||||
friend class santa::santad::event_providers::endpoint_security::MessagePeer;
|
||||
|
||||
private:
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi_;
|
||||
const es_message_t* es_msg_;
|
||||
std::optional<process_tree::ProcessToken> process_token_;
|
||||
|
||||
std::string GetProcessName(pid_t pid) const;
|
||||
|
||||
mutable enum StatChangeStep stat_change_step_ = StatChangeStep::kNoChange;
|
||||
mutable enum StatResult stat_result_ = StatResult::kOK;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
@@ -16,14 +16,16 @@
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libproc.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
|
||||
: esapi_(std::move(esapi)), es_msg_(es_msg) {
|
||||
: esapi_(std::move(esapi)), es_msg_(es_msg), process_token_(std::nullopt) {
|
||||
esapi_->RetainMessage(es_msg);
|
||||
UpdateStatState(StatChangeStep::kMessageCreate);
|
||||
}
|
||||
|
||||
Message::~Message() {
|
||||
@@ -36,12 +38,42 @@ Message::Message(Message &&other) {
|
||||
esapi_ = std::move(other.esapi_);
|
||||
es_msg_ = other.es_msg_;
|
||||
other.es_msg_ = nullptr;
|
||||
process_token_ = std::move(other.process_token_);
|
||||
other.process_token_ = std::nullopt;
|
||||
stat_change_step_ = other.stat_change_step_;
|
||||
stat_result_ = other.stat_result_;
|
||||
}
|
||||
|
||||
Message::Message(const Message &other) {
|
||||
esapi_ = other.esapi_;
|
||||
es_msg_ = other.es_msg_;
|
||||
esapi_->RetainMessage(es_msg_);
|
||||
process_token_ = other.process_token_;
|
||||
stat_change_step_ = other.stat_change_step_;
|
||||
stat_result_ = other.stat_result_;
|
||||
}
|
||||
|
||||
void Message::UpdateStatState(enum StatChangeStep step) const {
|
||||
// Only update state for AUTH EXEC events and if no previous change was detected
|
||||
if (es_msg_->event_type == ES_EVENT_TYPE_AUTH_EXEC &&
|
||||
stat_change_step_ == StatChangeStep::kNoChange &&
|
||||
// Note: The following checks are required due to tests that only
|
||||
// partially construct an es_message_t.
|
||||
es_msg_->event.exec.target && es_msg_->event.exec.target->executable) {
|
||||
struct stat &es_sb = es_msg_->event.exec.target->executable->stat;
|
||||
struct stat sb;
|
||||
int ret = stat(es_msg_->event.exec.target->executable->path.data, &sb);
|
||||
// If stat failed, or if devno/inode changed, update state.
|
||||
if (ret != 0 || es_sb.st_ino != sb.st_ino || es_sb.st_dev != sb.st_dev) {
|
||||
stat_change_step_ = step;
|
||||
// Determine the specific condition that failed for tracking purposes
|
||||
stat_result_ = (ret != 0) ? StatResult::kStatError : StatResult::kDevnoInodeMismatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Message::SetProcessToken(process_tree::ProcessToken tok) {
|
||||
process_token_ = std::move(tok);
|
||||
}
|
||||
|
||||
std::string Message::ParentProcessName() const {
|
||||
|
||||
@@ -21,11 +21,13 @@
|
||||
#include <stdlib.h>
|
||||
#include <sys/qos.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "Source/common/BranchPrediction.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/common/SystemResources.h"
|
||||
@@ -48,7 +50,9 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
"/private/var/db/santa/events.db"};
|
||||
|
||||
@interface SNTEndpointSecurityClient ()
|
||||
@property int64_t deadlineMarginMS;
|
||||
@property(nonatomic) double defaultBudget;
|
||||
@property(nonatomic) int64_t minAllowedHeadroom;
|
||||
@property(nonatomic) int64_t maxAllowedHeadroom;
|
||||
@property SNTConfigurator *configurator;
|
||||
@end
|
||||
|
||||
@@ -68,10 +72,18 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
if (self) {
|
||||
_esApi = std::move(esApi);
|
||||
_metrics = std::move(metrics);
|
||||
_deadlineMarginMS = 5000;
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
_processor = processor;
|
||||
|
||||
// Default event processing budget is 80% of the deadline time
|
||||
_defaultBudget = 0.8;
|
||||
|
||||
// For events with small deadlines, clamp processing budget to 1s headroom
|
||||
_minAllowedHeadroom = 1 * NSEC_PER_SEC;
|
||||
|
||||
// For events with large deadlines, clamp processing budget to 5s headroom
|
||||
_maxAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
|
||||
_authQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.auth_queue",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
|
||||
@@ -80,7 +92,7 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
_notifyQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.notify_queue",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_BACKGROUND, 0));
|
||||
QOS_CLASS_UTILITY, 0));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -116,6 +128,10 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (bool)handleContextMessage:(Message &)esMsg {
|
||||
return false;
|
||||
}
|
||||
|
||||
- (void)establishClientOrDie {
|
||||
if (self->_esClient.IsConnected()) {
|
||||
// This is a programming error
|
||||
@@ -130,18 +146,24 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
// sequence numbers are processed in order.
|
||||
self->_metrics->UpdateEventStats(self->_processor, esMsg.operator->());
|
||||
|
||||
es_event_type_t eventType = esMsg->event_type;
|
||||
if ([self handleContextMessage:esMsg]) {
|
||||
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
self->_metrics->SetEventMetrics(self->_processor, EventDisposition::kProcessed,
|
||||
processingEnd - processingStart, esMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self shouldHandleMessage:esMsg]) {
|
||||
[self handleMessage:std::move(esMsg)
|
||||
recordEventMetrics:^(EventDisposition disposition) {
|
||||
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
self->_metrics->SetEventMetrics(self->_processor, eventType, disposition,
|
||||
processingEnd - processingStart);
|
||||
self->_metrics->SetEventMetrics(self->_processor, disposition,
|
||||
processingEnd - processingStart, esMsg);
|
||||
}];
|
||||
} else {
|
||||
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
self->_metrics->SetEventMetrics(self->_processor, eventType, EventDisposition::kDropped,
|
||||
processingEnd - processingStart);
|
||||
self->_metrics->SetEventMetrics(self->_processor, EventDisposition::kDropped,
|
||||
processingEnd - processingStart, esMsg);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -255,6 +277,24 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
});
|
||||
}
|
||||
|
||||
- (int64_t)computeBudgetForDeadline:(uint64_t)deadline currentTime:(uint64_t)currentTime {
|
||||
// First get how much time we have left
|
||||
int64_t nanosUntilDeadline = (int64_t)MachTimeToNanos(deadline - currentTime);
|
||||
|
||||
// Compute the desired budget
|
||||
int64_t budget = nanosUntilDeadline * self.defaultBudget;
|
||||
|
||||
// See how much headroom is left
|
||||
int64_t headroom = nanosUntilDeadline - budget;
|
||||
|
||||
// Clamp headroom to maximize budget but ensure it's not so large as to not leave
|
||||
// enough time to respond in an emergency.
|
||||
headroom = std::clamp(headroom, self.minAllowedHeadroom, self.maxAllowedHeadroom);
|
||||
|
||||
// Return the processing budget given the allotted headroom
|
||||
return nanosUntilDeadline - headroom;
|
||||
}
|
||||
|
||||
- (void)processMessage:(Message &&)msg handler:(void (^)(const Message &))messageHandler {
|
||||
if (unlikely(msg->action_type != ES_ACTION_TYPE_AUTH)) {
|
||||
// This is a programming error
|
||||
@@ -270,33 +310,33 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
dispatch_semaphore_signal(processingSema);
|
||||
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
|
||||
|
||||
const uint64_t timeout = NSEC_PER_MSEC * (self.deadlineMarginMS);
|
||||
|
||||
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).
|
||||
int64_t processingBudget = [self computeBudgetForDeadline:msg->deadline
|
||||
currentTime:mach_absolute_time()];
|
||||
|
||||
// Workaround for compiler bug that doesn't properly close over variables
|
||||
__block Message processMsg = msg;
|
||||
__block Message deadlineMsg = msg;
|
||||
|
||||
dispatch_after(
|
||||
dispatch_time(DISPATCH_TIME_NOW, deadlineNano - timeout), self->_authQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler has already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, processingBudget), self->_authQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler has already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
bool res = [self respondToMessage:deadlineMsg
|
||||
withAuthResult:ES_AUTH_RESULT_DENY
|
||||
cacheable:false];
|
||||
es_auth_result_t authResult;
|
||||
if (self.configurator.failClosed) {
|
||||
authResult = ES_AUTH_RESULT_DENY;
|
||||
} else {
|
||||
authResult = ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
LOGE(@"SNTEndpointSecurityClient: deadline reached: deny pid=%d, event type: %d ret=%d",
|
||||
audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type, res);
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
bool res = [self respondToMessage:deadlineMsg withAuthResult:authResult cacheable:false];
|
||||
|
||||
LOGE(@"SNTEndpointSecurityClient: deadline reached: pid=%d, event type: %d, result: %@, ret=%d",
|
||||
audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type,
|
||||
(authResult == ES_AUTH_RESULT_DENY ? @"denied" : @"allowed"), res);
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
|
||||
dispatch_async(self->_authQueue, ^{
|
||||
messageHandler(processMsg);
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SystemResources.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
@@ -48,8 +50,11 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
- (void)handleMessage:(Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(santa::santad::EventDisposition disposition))recordEventMetrics;
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg;
|
||||
- (int64_t)computeBudgetForDeadline:(uint64_t)deadline currentTime:(uint64_t)currentTime;
|
||||
|
||||
@property int64_t deadlineMarginMS;
|
||||
@property(nonatomic) double defaultBudget;
|
||||
@property(nonatomic) int64_t minAllowedHeadroom;
|
||||
@property(nonatomic) int64_t maxAllowedHeadroom;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityClientTest : XCTestCase
|
||||
@@ -412,11 +417,11 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
{
|
||||
auto enrichedMsg = std::make_unique<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &esMsg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
auto enrichedMsg = std::make_unique<EnrichedMessage>(EnrichedClose(
|
||||
Message(mockESApi, &esMsg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt), std::nullopt),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
[client processEnrichedMessage:std::move(enrichedMsg)
|
||||
handler:^(std::unique_ptr<EnrichedMessage> msg) {
|
||||
@@ -503,7 +508,47 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testProcessMessageHandlerWithDeadlineTimeout {
|
||||
- (void)testComputeBudgetForDeadlineCurrentTime {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// The test uses crafted values to make even numbers. Ensure the client has
|
||||
// expected values for these properties so the test can fail early if not.
|
||||
XCTAssertEqual(client.defaultBudget, 0.8);
|
||||
XCTAssertEqual(client.minAllowedHeadroom, 1 * NSEC_PER_SEC);
|
||||
XCTAssertEqual(client.maxAllowedHeadroom, 5 * NSEC_PER_SEC);
|
||||
|
||||
std::map<uint64_t, int64_t> deadlineMillisToBudgetMillis{
|
||||
// Further out deadlines clamp processing budget to maxAllowedHeadroom
|
||||
{45000, 40000},
|
||||
|
||||
// Closer deadlines allow a set percentage processing budget
|
||||
{15000, 12000},
|
||||
|
||||
// Near deadlines clamp processing budget to minAllowedHeadroom
|
||||
{3500, 2500}};
|
||||
|
||||
uint64_t curTime = mach_absolute_time();
|
||||
|
||||
for (const auto [deadlineMS, budgetMS] : deadlineMillisToBudgetMillis) {
|
||||
int64_t got =
|
||||
[client computeBudgetForDeadline:AddNanosecondsToMachTime(deadlineMS * NSEC_PER_MSEC, curTime)
|
||||
currentTime:curTime];
|
||||
|
||||
// Add 100us, then clip to ms to account for non-exact values due to timebase division
|
||||
got = (int64_t)((double)(got + (100 * NSEC_PER_USEC)) / (double)NSEC_PER_MSEC);
|
||||
|
||||
XCTAssertEqual(got, budgetMS);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)checkDeadlineExpiredFailClosed:(BOOL)shouldFailClosed {
|
||||
// Set a es_message_t deadline of 750ms
|
||||
// Set a deadline leeway in the `SNTEndpointSecurityClient` of 500ms
|
||||
// Mock `RespondFlagsResult` which is called from the deadline handler
|
||||
@@ -517,7 +562,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
// deadlineSema is signaled (or a timeout waiting on deadlineSema)
|
||||
es_file_t proc_file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&proc_file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth,
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth,
|
||||
750); // 750ms timeout
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
@@ -526,18 +571,27 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
dispatch_semaphore_t deadlineSema = dispatch_semaphore_create(0);
|
||||
dispatch_semaphore_t controlSema = dispatch_semaphore_create(0);
|
||||
|
||||
EXPECT_CALL(*mockESApi, RespondFlagsResult(testing::_, testing::_, 0x0, false))
|
||||
es_auth_result_t wantAuthResult = shouldFailClosed ? ES_AUTH_RESULT_DENY : ES_AUTH_RESULT_ALLOW;
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, wantAuthResult, false))
|
||||
.WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
// Signal deadlineSema to let the handler block continue execution
|
||||
dispatch_semaphore_signal(deadlineSema);
|
||||
return true;
|
||||
}));
|
||||
|
||||
id mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([mockConfigurator configurator]).andReturn(mockConfigurator);
|
||||
|
||||
OCMExpect([mockConfigurator failClosed]).andReturn(shouldFailClosed);
|
||||
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
client.deadlineMarginMS = 500;
|
||||
|
||||
// Set min/max headroom the same to clamp the value for this test
|
||||
client.minAllowedHeadroom = 500 * NSEC_PER_MSEC;
|
||||
client.maxAllowedHeadroom = 500 * NSEC_PER_MSEC;
|
||||
|
||||
{
|
||||
__block long result;
|
||||
@@ -566,7 +620,18 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
// seeing the warning (but still possible)
|
||||
SleepMS(100);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockConfigurator));
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
[mockConfigurator stopMocking];
|
||||
}
|
||||
|
||||
- (void)testDeadlineExpiredFailClosed {
|
||||
[self checkDeadlineExpiredFailClosed:YES];
|
||||
}
|
||||
|
||||
- (void)testDeadlineExpiredFailOpen {
|
||||
[self checkDeadlineExpiredFailClosed:NO];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
@@ -50,6 +51,12 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode, FlushCacheReason reason));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityClient (Testing)
|
||||
@property(nonatomic) double defaultBudget;
|
||||
@property(nonatomic) int64_t minAllowedHeadroom;
|
||||
@property(nonatomic) int64_t maxAllowedHeadroom;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager (Testing)
|
||||
- (instancetype)init;
|
||||
- (void)logDiskAppeared:(NSDictionary *)props;
|
||||
@@ -136,6 +143,11 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
|
||||
// This test is sensitive to ~1s processing budget.
|
||||
// Set a 5s headroom and 6s deadline
|
||||
deviceManager.minAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
deviceManager.maxAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
es_message_t esMsg = MakeESMessage(eventType, &proc, ActionType::Auth, 6000);
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
@@ -375,16 +387,16 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
// Create mock disks with desired args
|
||||
MockDADisk * (^CreateMockDisk)(NSString *, NSString *) =
|
||||
^MockDADisk *(NSString *mountOn, NSString *mountFrom) {
|
||||
MockDADisk *mockDisk = [[MockDADisk alloc] init];
|
||||
mockDisk.diskDescription = @{
|
||||
@"DAVolumePath" : mountOn, // f_mntonname,
|
||||
@"DADevicePath" : mountOn, // f_mntonname,
|
||||
@"DAMediaBSDName" : mountFrom, // f_mntfromname,
|
||||
};
|
||||
|
||||
return mockDisk;
|
||||
MockDADisk *mockDisk = [[MockDADisk alloc] init];
|
||||
mockDisk.diskDescription = @{
|
||||
@"DAVolumePath" : mountOn, // f_mntonname,
|
||||
@"DADevicePath" : mountOn, // f_mntonname,
|
||||
@"DAMediaBSDName" : mountFrom, // f_mntfromname,
|
||||
};
|
||||
|
||||
return mockDisk;
|
||||
};
|
||||
|
||||
// Reset the Mock DA property, setup disks and remount args, then trigger the test
|
||||
void (^PerformStartupTest)(NSArray<MockDADisk *> *, NSArray<NSString *> *,
|
||||
SNTDeviceManagerStartupPreferences) =
|
||||
|
||||
@@ -17,15 +17,17 @@
|
||||
#import "Source/santad/EventProviders/AuthResultCache.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"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityTreeAwareClient.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#import "Source/santad/Metrics.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
|
||||
/// ES Client focused on subscribing to NOTIFY event variants with the intention of enriching
|
||||
/// received messages and logging the information.
|
||||
@interface SNTEndpointSecurityRecorder : SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
|
||||
@interface SNTEndpointSecurityRecorder
|
||||
: SNTEndpointSecurityTreeAwareClient <SNTEndpointSecurityEventHandler>
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
@@ -38,6 +40,7 @@
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:
|
||||
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache
|
||||
prefixTree:(std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>)prefixTree;
|
||||
prefixTree:(std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>)prefixTree
|
||||
processTree:(std::shared_ptr<santa::santad::process_tree::ProcessTree>)processTree;
|
||||
|
||||
@end
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h"
|
||||
#include <os/base.h>
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
@@ -23,6 +24,7 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
@@ -33,10 +35,12 @@ using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
using santa::santad::process_tree::ProcessTree;
|
||||
|
||||
es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
switch (msg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: return msg->event.close.target;
|
||||
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: return msg->event.exchangedata.file1;
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: return msg->event.link.source;
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: return msg->event.rename.source;
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: return msg->event.unlink.target;
|
||||
@@ -62,10 +66,12 @@ 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<PrefixTree<Unit>>)prefixTree {
|
||||
prefixTree:(std::shared_ptr<PrefixTree<Unit>>)prefixTree
|
||||
processTree:(std::shared_ptr<ProcessTree>)processTree {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:std::move(metrics)
|
||||
processor:santa::santad::Processor::kRecorder];
|
||||
processor:santa::santad::Processor::kRecorder
|
||||
processTree:std::move(processTree)];
|
||||
if (self) {
|
||||
_enricher = enricher;
|
||||
_logger = logger;
|
||||
@@ -91,10 +97,10 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
BOOL shouldLogClose = esMsg->event.close.modified;
|
||||
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.5, *)) {
|
||||
if (esMsg->version >= 6) {
|
||||
// As of macSO 13.0 we have a new field for if a file was mmaped with
|
||||
// write permissions on close events. However it did not work until
|
||||
// 13.5.
|
||||
// write permissions on close events. However due to a bug in ES, it
|
||||
// only worked for certain conditions until macOS 13.5 (FB12094635).
|
||||
//
|
||||
// If something was mmaped writable it was probably written to. Often
|
||||
// developer tools do this to avoid lots of write syscalls, e.g. go's
|
||||
@@ -114,8 +120,28 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
|
||||
self->_authResultCache->RemoveFromCache(esMsg->event.close.target);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
[self.compilerController handleEvent:esMsg withLogger:self->_logger];
|
||||
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: OS_FALLTHROUGH;
|
||||
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: OS_FALLTHROUGH;
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: OS_FALLTHROUGH;
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: OS_FALLTHROUGH;
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: {
|
||||
es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg));
|
||||
|
||||
if (!targetFile) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Only log file changes that match the given regex
|
||||
NSString *targetPath = santa::common::StringToNSString(esMsg->event.close.target->path.data);
|
||||
NSString *targetPath = santa::common::StringToNSString(targetFile->path.data);
|
||||
if (![[self.configurator fileChangesRegex]
|
||||
numberOfMatchesInString:targetPath
|
||||
options:0
|
||||
@@ -127,27 +153,26 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->_prefixTree->HasPrefix(targetFile->path.data)) {
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK: OS_FALLTHROUGH;
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT: {
|
||||
if (self.configurator.enableForkAndExitLogging == NO) {
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
[self.compilerController handleEvent:esMsg withLogger:self->_logger];
|
||||
|
||||
if ((esMsg->event_type == ES_EVENT_TYPE_NOTIFY_FORK ||
|
||||
esMsg->event_type == ES_EVENT_TYPE_NOTIFY_EXIT) &&
|
||||
self.configurator.enableForkAndExitLogging == NO) {
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter file op events matching the prefix tree.
|
||||
es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg));
|
||||
if (targetFile != NULL && self->_prefixTree->HasPrefix(targetFile->path.data)) {
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enrich the message inline with the ES handler block to capture enrichment
|
||||
// data as close to the source event as possible.
|
||||
std::unique_ptr<EnrichedMessage> enrichedMessage = _enricher->Enrich(std::move(esMsg));
|
||||
|
||||
@@ -103,7 +103,7 @@ class MockLogger : public Logger {
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
typedef void (^testHelperBlock)(es_message_t *message,
|
||||
typedef void (^TestHelperBlock)(es_message_t *message,
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient,
|
||||
std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
@@ -114,7 +114,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
- (void)handleMessageShouldLog:(BOOL)shouldLog
|
||||
shouldRemoveFromCache:(BOOL)shouldRemoveFromCache
|
||||
withBlock:(testHelperBlock)testBlock {
|
||||
withBlock:(TestHelperBlock)testBlock {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth);
|
||||
@@ -157,7 +157,8 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
enricher:mockEnricher
|
||||
compilerController:mockCC
|
||||
authResultCache:mockAuthCache
|
||||
prefixTree:prefixTree];
|
||||
prefixTree:prefixTree
|
||||
processTree:nullptr];
|
||||
|
||||
testBlock(&esMsg, mockESApi, mockCC, recorderClient, prefixTree, &sema, &semaMetrics);
|
||||
|
||||
@@ -176,7 +177,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
if (@available(macOS 13.0, *)) {
|
||||
// CLOSE not modified, but was_mapped_writable, should remove from cache,
|
||||
// and matches fileChangesRegex
|
||||
testHelperBlock testBlock =
|
||||
TestHelperBlock testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema,
|
||||
@@ -208,7 +209,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
if (@available(macOS 13.0, *)) {
|
||||
// CLOSE not modified, but was_mapped_writable, remove from cache, and does not match
|
||||
// fileChangesRegex
|
||||
testHelperBlock testBlock =
|
||||
TestHelperBlock testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema,
|
||||
@@ -232,7 +233,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
- (void)testHandleMessage {
|
||||
// CLOSE not modified, bail early
|
||||
testHelperBlock testBlock = ^(
|
||||
TestHelperBlock testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
@@ -281,6 +282,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &targetFileMissesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
@@ -289,6 +291,44 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
[self handleMessageShouldLog:NO shouldRemoveFromCache:YES withBlock:testBlock];
|
||||
|
||||
// UNLINK, remove from cache, but doesn't match fileChangesRegex
|
||||
testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_UNLINK;
|
||||
esMsg->event.unlink.target = &targetFileMissesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
}]);
|
||||
};
|
||||
|
||||
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
|
||||
|
||||
// EXCHANGEDATA, Prefix match, bail early
|
||||
testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_UNLINK;
|
||||
esMsg->event.exchangedata.file1 = &targetFileMatchesRegex;
|
||||
prefixTree->InsertPrefix(esMsg->event.exchangedata.file1->path.data, Unit{});
|
||||
Message msg(mockESApi, esMsg);
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(*semaMetrics);
|
||||
}]);
|
||||
|
||||
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
};
|
||||
|
||||
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
|
||||
|
||||
// LINK, Prefix match, bail early
|
||||
testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
@@ -371,6 +411,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
extern es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg);
|
||||
|
||||
es_file_t closeFile = MakeESFile("close");
|
||||
es_file_t exchangedataFile = MakeESFile("exchangedata");
|
||||
es_file_t linkFile = MakeESFile("link");
|
||||
es_file_t renameFile = MakeESFile("rename");
|
||||
es_file_t unlinkFile = MakeESFile("unlink");
|
||||
@@ -393,7 +434,8 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &unlinkFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
esMsg.event.exchangedata.file1 = &exchangedataFile;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &exchangedataFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/// Copyright 2024 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/EventProviders/SNTEndpointSecurityClient.h"
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
@interface SNTEndpointSecurityTreeAwareClient : SNTEndpointSecurityClient
|
||||
@property std::shared_ptr<santa::santad::process_tree::ProcessTree> processTree;
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
processor:(santa::santad::Processor)processor
|
||||
processTree:(std::shared_ptr<santa::santad::process_tree::ProcessTree>)processTree;
|
||||
@end
|
||||
@@ -0,0 +1,114 @@
|
||||
/// Copyright 2024 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/SNTEndpointSecurityTreeAwareClient.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#include "Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#include "Source/santad/ProcessTree/process_tree_macos.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::Metrics;
|
||||
using santa::santad::Processor;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
@implementation SNTEndpointSecurityTreeAwareClient {
|
||||
std::vector<bool> _addedEvents;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<Metrics>)metrics
|
||||
processor:(Processor)processor
|
||||
processTree:
|
||||
(std::shared_ptr<santa::santad::process_tree::ProcessTree>)processTree {
|
||||
self = [super initWithESAPI:std::move(esApi) metrics:std::move(metrics) processor:processor];
|
||||
if (self) {
|
||||
_processTree = std::move(processTree);
|
||||
_addedEvents.resize(ES_EVENT_TYPE_LAST, false);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// ES guarantees logical consistency within a client (e.g. forks always precede exits),
|
||||
// however there are no guarantees about the ordering of when messages are delivered _across_
|
||||
// clients, meaning any client might be the first one to receive process events, and therefore would
|
||||
// need to be the one to inform the tree. However not all clients are interested in or subscribe to
|
||||
// process events. This (and the below handleContextMessage) ensures that the ES subscription for
|
||||
// all clients includes the minimal required set of events for process tree (NOTIFY_FORK, some EXEC
|
||||
// variant, and NOTIFY_EXIT) but also filters out any events that were subscribed to solely for the
|
||||
// purpose of updating the tree from being processed downstream, where they would be unexpected.
|
||||
- (bool)subscribe:(const std::set<es_event_type_t> &)events {
|
||||
std::set<es_event_type_t> eventsWithLifecycle = events;
|
||||
if (events.find(ES_EVENT_TYPE_NOTIFY_FORK) == events.end()) {
|
||||
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_FORK);
|
||||
_addedEvents[ES_EVENT_TYPE_NOTIFY_FORK] = true;
|
||||
}
|
||||
if (events.find(ES_EVENT_TYPE_NOTIFY_EXEC) == events.end() &&
|
||||
events.find(ES_EVENT_TYPE_AUTH_EXEC) == events.end()) {
|
||||
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_EXEC);
|
||||
_addedEvents[ES_EVENT_TYPE_NOTIFY_EXEC] = true;
|
||||
}
|
||||
if (events.find(ES_EVENT_TYPE_NOTIFY_EXIT) == events.end()) {
|
||||
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_EXIT);
|
||||
_addedEvents[ES_EVENT_TYPE_NOTIFY_EXIT] = true;
|
||||
}
|
||||
|
||||
return [super subscribe:eventsWithLifecycle];
|
||||
}
|
||||
|
||||
- (bool)handleContextMessage:(Message &)esMsg {
|
||||
if (!_processTree) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Inform the tree
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK:
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC:
|
||||
case ES_EVENT_TYPE_AUTH_EXEC:
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT:
|
||||
santa::santad::process_tree::InformFromESEvent(*_processTree, esMsg);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Now enumerate the processes that processing this event might require access to...
|
||||
std::vector<struct santa::santad::process_tree::Pid> pids;
|
||||
pids.emplace_back(santa::santad::process_tree::PidFromAuditToken(esMsg->process->audit_token));
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_EXEC:
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC:
|
||||
pids.emplace_back(
|
||||
santa::santad::process_tree::PidFromAuditToken(esMsg->event.exec.target->audit_token));
|
||||
break;
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK:
|
||||
pids.emplace_back(
|
||||
santa::santad::process_tree::PidFromAuditToken(esMsg->event.fork.child->audit_token));
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
// ...and create the token for those.
|
||||
esMsg.SetProcessToken(santa::santad::process_tree::ProcessToken(_processTree, std::move(pids)));
|
||||
|
||||
return _addedEvents[esMsg->event_type];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -143,11 +143,11 @@ class MockWriter : public Null {
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
{
|
||||
auto enrichedMsg = std::make_unique<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
auto enrichedMsg = std::make_unique<EnrichedMessage>(EnrichedClose(
|
||||
Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt), std::nullopt),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
|
||||
EXPECT_CALL(*mockWriter, Write).Times(1);
|
||||
@@ -229,10 +229,11 @@ class MockWriter : public Null {
|
||||
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);
|
||||
.LogFileAccess(
|
||||
"v1", "name", Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt), std::nullopt),
|
||||
"tgt", FileAccessPolicyDecision::kDenied);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
|
||||
@@ -94,12 +94,14 @@ std::string GetReasonString(SNTEventState event_state) {
|
||||
case SNTEventStateAllowScope: return "SCOPE";
|
||||
case SNTEventStateAllowTeamID: return "TEAMID";
|
||||
case SNTEventStateAllowSigningID: return "SIGNINGID";
|
||||
case SNTEventStateAllowCDHash: return "CDHASH";
|
||||
case SNTEventStateAllowUnknown: return "UNKNOWN";
|
||||
case SNTEventStateBlockBinary: return "BINARY";
|
||||
case SNTEventStateBlockCertificate: return "CERT";
|
||||
case SNTEventStateBlockScope: return "SCOPE";
|
||||
case SNTEventStateBlockTeamID: return "TEAMID";
|
||||
case SNTEventStateBlockSigningID: return "SIGNINGID";
|
||||
case SNTEventStateBlockCDHash: return "CDHASH";
|
||||
case SNTEventStateBlockLongPath: return "LONG_PATH";
|
||||
case SNTEventStateBlockUnknown: return "UNKNOWN";
|
||||
default: return "NOTRUNNING";
|
||||
|
||||
@@ -436,6 +436,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
{SNTEventStateBlockScope, "SCOPE"},
|
||||
{SNTEventStateBlockTeamID, "TEAMID"},
|
||||
{SNTEventStateBlockSigningID, "SIGNINGID"},
|
||||
{SNTEventStateBlockCDHash, "CDHASH"},
|
||||
{SNTEventStateBlockLongPath, "LONG_PATH"},
|
||||
{SNTEventStateAllowUnknown, "UNKNOWN"},
|
||||
{SNTEventStateAllowBinary, "BINARY"},
|
||||
@@ -446,6 +447,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
{SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"},
|
||||
{SNTEventStateAllowTeamID, "TEAMID"},
|
||||
{SNTEventStateAllowSigningID, "SIGNINGID"},
|
||||
{SNTEventStateAllowCDHash, "CDHASH"},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
|
||||
@@ -185,6 +185,14 @@ static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, const es_
|
||||
pb_file->set_truncated(es_file->path_truncated);
|
||||
}
|
||||
|
||||
static inline void EncodeAnnotations(std::function<::pbv1::process_tree::Annotations *()> lazy_f,
|
||||
const EnrichedProcess &enriched_proc) {
|
||||
if (std::optional<pbv1::process_tree::Annotations> proc_annotations = enriched_proc.annotations();
|
||||
proc_annotations) {
|
||||
*lazy_f() = *proc_annotations;
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeProcessInfoLight(::pbv1::ProcessInfoLight *pb_proc_info,
|
||||
uint32_t message_version, const es_process_t *es_proc,
|
||||
const EnrichedProcess &enriched_proc) {
|
||||
@@ -205,6 +213,8 @@ static inline void EncodeProcessInfoLight(::pbv1::ProcessInfoLight *pb_proc_info
|
||||
enriched_proc.real_group());
|
||||
|
||||
EncodeFileInfoLight(pb_proc_info->mutable_executable(), es_proc->executable);
|
||||
|
||||
EncodeAnnotations([pb_proc_info] { return pb_proc_info->mutable_annotations(); }, enriched_proc);
|
||||
}
|
||||
|
||||
static inline void EncodeProcessInfo(::pbv1::ProcessInfo *pb_proc_info, uint32_t message_version,
|
||||
@@ -256,6 +266,8 @@ static inline void EncodeProcessInfo(::pbv1::ProcessInfo *pb_proc_info, uint32_t
|
||||
if (message_version >= 3) {
|
||||
EncodeTimestamp(pb_proc_info->mutable_start_time(), es_proc->start_time);
|
||||
}
|
||||
|
||||
EncodeAnnotations([pb_proc_info] { return pb_proc_info->mutable_annotations(); }, enriched_proc);
|
||||
}
|
||||
|
||||
void EncodeExitStatus(::pbv1::Exit *pb_exit, int exitStatus) {
|
||||
@@ -299,12 +311,14 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
case SNTEventStateAllowScope: return ::pbv1::Execution::REASON_SCOPE;
|
||||
case SNTEventStateAllowTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
|
||||
case SNTEventStateAllowSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
|
||||
case SNTEventStateAllowCDHash: return ::pbv1::Execution::REASON_CDHASH;
|
||||
case SNTEventStateAllowUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
|
||||
case SNTEventStateBlockBinary: return ::pbv1::Execution::REASON_BINARY;
|
||||
case SNTEventStateBlockCertificate: return ::pbv1::Execution::REASON_CERT;
|
||||
case SNTEventStateBlockScope: return ::pbv1::Execution::REASON_SCOPE;
|
||||
case SNTEventStateBlockTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
|
||||
case SNTEventStateBlockSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
|
||||
case SNTEventStateBlockCDHash: return ::pbv1::Execution::REASON_CDHASH;
|
||||
case SNTEventStateBlockLongPath: return ::pbv1::Execution::REASON_LONG_PATH;
|
||||
case SNTEventStateBlockUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
|
||||
default: return ::pbv1::Execution::REASON_NOT_RUNNING;
|
||||
@@ -367,7 +381,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
|
||||
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, struct timespec event_time,
|
||||
struct timespec processed_time) {
|
||||
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
|
||||
::pbv1::SantaMessage *santa_msg = Arena::Create<::pbv1::SantaMessage>(arena);
|
||||
|
||||
if (EnabledMachineID()) {
|
||||
EncodeString([santa_msg] { return santa_msg->mutable_machine_id(); }, MachineID());
|
||||
@@ -401,7 +415,7 @@ std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
|
||||
// TODO: Profile this. It's probably not the most efficient way to do this.
|
||||
JsonPrintOptions options;
|
||||
options.always_print_enums_as_ints = false;
|
||||
options.always_print_primitive_fields = true;
|
||||
options.always_print_fields_with_no_presence = true;
|
||||
options.preserve_proto_field_names = true;
|
||||
std::string json;
|
||||
|
||||
|
||||
@@ -78,30 +78,21 @@ using santa::santad::logs::endpoint_security::serializers::GetModeEnum;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetPolicyDecision;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetReasonEnum;
|
||||
|
||||
@interface ProtobufTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property id mockDecisionCache;
|
||||
@property SNTCachedDecision *testCachedDecision;
|
||||
@end
|
||||
|
||||
JsonPrintOptions DefaultJsonPrintOptions() {
|
||||
JsonPrintOptions options;
|
||||
options.always_print_enums_as_ints = false;
|
||||
options.always_print_primitive_fields = false;
|
||||
options.always_print_fields_with_no_presence = false;
|
||||
options.preserve_proto_field_names = true;
|
||||
options.add_whitespace = true;
|
||||
return options;
|
||||
}
|
||||
|
||||
NSString *TestJsonPath(NSString *jsonFileName, uint32_t version) {
|
||||
static dispatch_once_t onceToken;
|
||||
static NSString *testPath;
|
||||
static NSString *testDataRepoPath = @"santa/Source/santad/testdata/protobuf";
|
||||
NSString *testDataRepoVersionPath = [NSString stringWithFormat:@"v%u", version];
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
testPath = [NSString pathWithComponents:@[
|
||||
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testDataRepoPath
|
||||
]];
|
||||
});
|
||||
|
||||
return [NSString pathWithComponents:@[ testPath, testDataRepoVersionPath, jsonFileName ]];
|
||||
}
|
||||
|
||||
NSString *EventTypeToFilename(es_event_type_t eventType) {
|
||||
switch (eventType) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: return @"close.json";
|
||||
@@ -117,6 +108,16 @@ NSString *EventTypeToFilename(es_event_type_t eventType) {
|
||||
}
|
||||
}
|
||||
|
||||
NSString *TestJsonPath(NSString *jsonFileName, uint32_t version) {
|
||||
NSString *p = [NSString pathWithComponents:@[
|
||||
[[NSBundle bundleForClass:[ProtobufTest class]] resourcePath],
|
||||
@"protobuf",
|
||||
[NSString stringWithFormat:@"v%u", version],
|
||||
jsonFileName,
|
||||
]];
|
||||
return p;
|
||||
}
|
||||
|
||||
NSString *LoadTestJson(NSString *jsonFileName, uint32_t version) {
|
||||
NSError *err = nil;
|
||||
NSString *jsonData = [NSString stringWithContentsOfFile:TestJsonPath(jsonFileName, version)
|
||||
@@ -325,12 +326,6 @@ void SerializeAndCheckNonESEvents(
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
@interface ProtobufTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property id mockDecisionCache;
|
||||
@property SNTCachedDecision *testCachedDecision;
|
||||
@end
|
||||
|
||||
@implementation ProtobufTest
|
||||
|
||||
- (void)setUp {
|
||||
@@ -443,6 +438,7 @@ void SerializeAndCheckNonESEvents(
|
||||
{SNTEventStateBlockScope, ::pbv1::Execution::REASON_SCOPE},
|
||||
{SNTEventStateBlockTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
{SNTEventStateBlockSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
|
||||
{SNTEventStateBlockCDHash, ::pbv1::Execution::REASON_CDHASH},
|
||||
{SNTEventStateBlockLongPath, ::pbv1::Execution::REASON_LONG_PATH},
|
||||
{SNTEventStateAllowUnknown, ::pbv1::Execution::REASON_UNKNOWN},
|
||||
{SNTEventStateAllowBinary, ::pbv1::Execution::REASON_BINARY},
|
||||
@@ -453,6 +449,7 @@ void SerializeAndCheckNonESEvents(
|
||||
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::REASON_PENDING_TRANSITIVE},
|
||||
{SNTEventStateAllowTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
{SNTEventStateAllowSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
|
||||
{SNTEventStateAllowCDHash, ::pbv1::Execution::REASON_CDHASH},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool_platform_specific.h"
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
|
||||
namespace fsspool {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
namespace santa::santad {
|
||||
|
||||
@@ -54,6 +55,7 @@ enum class FileAccessMetricStatus {
|
||||
using EventCountTuple = std::tuple<Processor, es_event_type_t, EventDisposition>;
|
||||
using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
|
||||
using EventStatsTuple = std::tuple<Processor, es_event_type_t>;
|
||||
using EventStatChangeTuple = std::tuple<StatChangeStep, StatResult>;
|
||||
using FileAccessMetricsPolicyVersion = std::string;
|
||||
using FileAccessMetricsPolicyName = std::string;
|
||||
using FileAccessEventCountTuple =
|
||||
@@ -67,8 +69,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *drop_counts,
|
||||
SNTMetricCounter *faa_event_counts, SNTMetricSet *metric_set,
|
||||
void (^run_on_first_start)(Metrics *));
|
||||
SNTMetricCounter *faa_event_counts, SNTMetricCounter *stat_change_counts,
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *));
|
||||
|
||||
~Metrics();
|
||||
|
||||
@@ -83,8 +85,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
// Used for tracking event sequence numbers to determine if drops occured
|
||||
void UpdateEventStats(Processor processor, const es_message_t *msg);
|
||||
|
||||
void SetEventMetrics(Processor processor, es_event_type_t event_type,
|
||||
EventDisposition disposition, int64_t nanos);
|
||||
void SetEventMetrics(Processor processor, EventDisposition event_disposition, int64_t nanos,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg);
|
||||
|
||||
void SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count);
|
||||
|
||||
@@ -112,6 +114,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
SNTMetricCounter *rate_limit_counts_;
|
||||
SNTMetricCounter *faa_event_counts_;
|
||||
SNTMetricCounter *drop_counts_;
|
||||
SNTMetricCounter *stat_change_counts_;
|
||||
SNTMetricSet *metric_set_;
|
||||
// Tracks whether or not the timer_source should be running.
|
||||
// This helps manage dispatch source state to ensure the source is not
|
||||
@@ -129,6 +132,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
std::map<Processor, int64_t> rate_limit_counts_cache_;
|
||||
std::map<FileAccessEventCountTuple, int64_t> faa_event_counts_cache_;
|
||||
std::map<EventStatsTuple, SequenceStats> drop_cache_;
|
||||
std::map<EventStatChangeTuple, int64_t> stat_change_cache_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
|
||||
#include <memory>
|
||||
@@ -54,6 +55,14 @@ static NSString *const kPseudoEventTypeGlobal = @"Global";
|
||||
static NSString *const kEventDispositionDropped = @"Dropped";
|
||||
static NSString *const kEventDispositionProcessed = @"Processed";
|
||||
|
||||
static NSString *const kStatChangeStepNoChange = @"NoChange";
|
||||
static NSString *const kStatChangeStepMessageCreate = @"MessageCreate";
|
||||
static NSString *const kStatChangeStepCodesignValidation = @"CodesignValidation";
|
||||
|
||||
static NSString *const kStatResultOK = @"OK";
|
||||
static NSString *const kStatResultStatError = @"StatError";
|
||||
static NSString *const kStatResultDevnoInodeMismatch = @"DevnoInodeMismatch";
|
||||
|
||||
// Compat values
|
||||
static NSString *const kFileAccessMetricStatusOK = @"OK";
|
||||
static NSString *const kFileAccessMetricStatusBlockedUser = @"BLOCKED_USER";
|
||||
@@ -148,6 +157,30 @@ NSString *const FileAccessPolicyDecisionToString(FileAccessPolicyDecision decisi
|
||||
}
|
||||
}
|
||||
|
||||
NSString *const StatChangeStepToString(StatChangeStep step) {
|
||||
switch (step) {
|
||||
case StatChangeStep::kNoChange: return kStatChangeStepNoChange;
|
||||
case StatChangeStep::kMessageCreate: return kStatChangeStepMessageCreate;
|
||||
case StatChangeStep::kCodesignValidation: return kStatChangeStepCodesignValidation;
|
||||
default:
|
||||
[NSException raise:@"Invalid stat change step"
|
||||
format:@"Unknown stat change step value: %d", static_cast<int>(step)];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSString *const StatResultToString(StatResult result) {
|
||||
switch (result) {
|
||||
case StatResult::kOK: return kStatResultOK;
|
||||
case StatResult::kStatError: return kStatResultStatError;
|
||||
case StatResult::kDevnoInodeMismatch: return kStatResultDevnoInodeMismatch;
|
||||
default:
|
||||
[NSException raise:@"Invalid stat result"
|
||||
format:@"Unknown stat result value: %d", static_cast<int>(result)];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t interval) {
|
||||
dispatch_queue_t q = dispatch_queue_create("com.google.santa.santametricsservice.q",
|
||||
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
|
||||
@@ -181,9 +214,14 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
|
||||
fieldNames:@[ @"Processor", @"Event" ]
|
||||
helpText:@"Count of the number of drops for each event"];
|
||||
|
||||
SNTMetricCounter *stat_change_counts =
|
||||
[metric_set counterWithName:@"/santa/event_stat_change_count"
|
||||
fieldNames:@[ @"step", @"error" ]
|
||||
helpText:@"Count of times a stat info changed for a binary being evalauted"];
|
||||
|
||||
std::shared_ptr<Metrics> metrics = std::make_shared<Metrics>(
|
||||
q, timer_source, interval, event_processing_times, event_counts, rate_limit_counts,
|
||||
faa_event_counts, drop_counts, metric_set, ^(Metrics *metrics) {
|
||||
faa_event_counts, drop_counts, stat_change_counts, metric_set, ^(Metrics *metrics) {
|
||||
SNTRegisterCoreMetrics();
|
||||
metrics->EstablishConnection();
|
||||
});
|
||||
@@ -204,8 +242,8 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
|
||||
Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
|
||||
SNTMetricCounter *drop_counts, SNTMetricSet *metric_set,
|
||||
void (^run_on_first_start)(Metrics *))
|
||||
SNTMetricCounter *drop_counts, SNTMetricCounter *stat_change_counts,
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *))
|
||||
: q_(q),
|
||||
timer_source_(timer_source),
|
||||
interval_(interval),
|
||||
@@ -214,6 +252,7 @@ Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t in
|
||||
rate_limit_counts_(rate_limit_counts),
|
||||
faa_event_counts_(faa_event_counts),
|
||||
drop_counts_(drop_counts),
|
||||
stat_change_counts_(stat_change_counts),
|
||||
metric_set_(metric_set),
|
||||
run_on_first_start_(run_on_first_start) {
|
||||
SetInterval(interval_);
|
||||
@@ -307,6 +346,15 @@ void Metrics::FlushMetrics() {
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &[key, count] : stat_change_cache_) {
|
||||
if (count > 0) {
|
||||
NSString *stepName = StatChangeStepToString(std::get<StatChangeStep>(key));
|
||||
NSString *error = StatResultToString(std::get<StatResult>(key));
|
||||
|
||||
[stat_change_counts_ incrementBy:count forFieldValues:@[ stepName, error ]];
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the maps so the next cycle begins with a clean state
|
||||
// IMPORTANT: Do not reset drop_cache_, the sequence numbers must persist
|
||||
// for accurate accounting
|
||||
@@ -314,6 +362,7 @@ void Metrics::FlushMetrics() {
|
||||
event_times_cache_ = {};
|
||||
rate_limit_counts_cache_ = {};
|
||||
faa_event_counts_cache_ = {};
|
||||
stat_change_cache_ = {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -355,11 +404,17 @@ void Metrics::StopPoll() {
|
||||
});
|
||||
}
|
||||
|
||||
void Metrics::SetEventMetrics(Processor processor, es_event_type_t event_type,
|
||||
EventDisposition event_disposition, int64_t nanos) {
|
||||
void Metrics::SetEventMetrics(
|
||||
Processor processor, EventDisposition event_disposition, int64_t nanos,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg) {
|
||||
dispatch_sync(events_q_, ^{
|
||||
event_counts_cache_[EventCountTuple{processor, event_type, event_disposition}]++;
|
||||
event_times_cache_[EventTimesTuple{processor, event_type}] = nanos;
|
||||
event_counts_cache_[EventCountTuple{processor, msg->event_type, event_disposition}]++;
|
||||
event_times_cache_[EventTimesTuple{processor, msg->event_type}] = nanos;
|
||||
|
||||
// Stat changes are only tracked for AUTH EXEC events
|
||||
if (msg->event_type == ES_EVENT_TYPE_AUTH_EXEC) {
|
||||
stat_change_cache_[EventStatChangeTuple{msg.StatChangeStep(), msg.StatResult()}]++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
@@ -19,13 +21,17 @@
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/common/SNTMetricSet.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
|
||||
using santa::santad::EventCountTuple;
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::EventStatChangeTuple;
|
||||
using santa::santad::EventStatsTuple;
|
||||
using santa::santad::EventTimesTuple;
|
||||
using santa::santad::FileAccessEventCountTuple;
|
||||
@@ -38,6 +44,8 @@ extern NSString *const EventTypeToString(es_event_type_t eventType);
|
||||
extern NSString *const EventDispositionToString(EventDisposition d);
|
||||
extern NSString *const FileAccessMetricStatusToString(FileAccessMetricStatus status);
|
||||
extern NSString *const FileAccessPolicyDecisionToString(FileAccessPolicyDecision decision);
|
||||
extern NSString *const StatChangeStepToString(StatChangeStep decision);
|
||||
extern NSString *const StatResultToString(StatResult decision);
|
||||
|
||||
class MetricsPeer : public Metrics {
|
||||
public:
|
||||
@@ -55,12 +63,27 @@ class MetricsPeer : public Metrics {
|
||||
using Metrics::interval_;
|
||||
using Metrics::rate_limit_counts_cache_;
|
||||
using Metrics::running_;
|
||||
using Metrics::stat_change_cache_;
|
||||
|
||||
using Metrics::SequenceStats;
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class MessagePeer : public Message {
|
||||
public:
|
||||
// Make base class constructors visible
|
||||
using Message::Message;
|
||||
|
||||
// Private member variables
|
||||
using Message::stat_change_step_;
|
||||
using Message::stat_result_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
using santa::santad::EventDispositionToString;
|
||||
using santa::santad::EventTypeToString;
|
||||
using santa::santad::FileAccessMetricStatus;
|
||||
@@ -69,10 +92,13 @@ using santa::santad::FileAccessPolicyDecisionToString;
|
||||
using santa::santad::Metrics;
|
||||
using santa::santad::MetricsPeer;
|
||||
using santa::santad::ProcessorToString;
|
||||
using santa::santad::StatChangeStepToString;
|
||||
using santa::santad::StatResultToString;
|
||||
using santa::santad::event_providers::endpoint_security::MessagePeer;
|
||||
|
||||
std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^block)(Metrics *)) {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
|
||||
return std::make_shared<MetricsPeer>(q, timer, 100, nil, nil, nil, nil, nil, nil, block);
|
||||
return std::make_shared<MetricsPeer>(q, timer, 100, nil, nil, nil, nil, nil, nil, nil, block);
|
||||
}
|
||||
|
||||
@interface MetricsTest : XCTestCase
|
||||
@@ -228,8 +254,44 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testStatChangeStepToString {
|
||||
std::map<StatChangeStep, NSString *> stepToString = {
|
||||
{StatChangeStep::kNoChange, @"NoChange"},
|
||||
{StatChangeStep::kMessageCreate, @"MessageCreate"},
|
||||
{StatChangeStep::kCodesignValidation, @"CodesignValidation"},
|
||||
};
|
||||
|
||||
for (const auto &kv : stepToString) {
|
||||
XCTAssertEqualObjects(StatChangeStepToString(kv.first), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(StatChangeStepToString((StatChangeStep)12345));
|
||||
}
|
||||
|
||||
- (void)testStatResultToString {
|
||||
std::map<StatResult, NSString *> resultToString = {
|
||||
{StatResult::kOK, @"OK"},
|
||||
{StatResult::kStatError, @"StatError"},
|
||||
{StatResult::kDevnoInodeMismatch, @"DevnoInodeMismatch"},
|
||||
};
|
||||
|
||||
for (const auto &kv : resultToString) {
|
||||
XCTAssertEqualObjects(StatResultToString(kv.first), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(StatResultToString((StatResult)12345));
|
||||
}
|
||||
|
||||
- (void)testSetEventMetrics {
|
||||
int64_t nanos = 1234;
|
||||
es_message_t esExecMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, NULL);
|
||||
es_message_t esOpenMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, NULL);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
MessagePeer execMsg(mockESApi, &esExecMsg);
|
||||
MessagePeer openMsg(mockESApi, &esOpenMsg);
|
||||
|
||||
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
|
||||
});
|
||||
@@ -237,22 +299,27 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
|
||||
// Initial maps are empty
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->stat_change_cache_.size(), 0);
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
EventDisposition::kProcessed, nanos);
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos, execMsg);
|
||||
|
||||
// Check sizes after setting metrics once
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 1);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 1);
|
||||
XCTAssertEqual(metrics->stat_change_cache_.size(), 1);
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
EventDisposition::kProcessed, nanos);
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN,
|
||||
EventDisposition::kProcessed, nanos * 2);
|
||||
execMsg.stat_change_step_ = StatChangeStep::kMessageCreate;
|
||||
execMsg.stat_result_ = StatResult::kDevnoInodeMismatch;
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos, execMsg);
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos * 2,
|
||||
openMsg);
|
||||
|
||||
// Re-check expected counts. One was an update, so should only be 2 items
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 2);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
|
||||
// Stat change counts should be 2 because one call wasn't an AUTH EXEC event
|
||||
XCTAssertEqual(metrics->stat_change_cache_.size(), 2);
|
||||
|
||||
// Check map values
|
||||
EventCountTuple ecExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
@@ -261,11 +328,16 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
|
||||
EventDisposition::kProcessed};
|
||||
EventTimesTuple etExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC};
|
||||
EventTimesTuple etOpen{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN};
|
||||
EventStatChangeTuple noChange{StatChangeStep::kNoChange, StatResult::kOK};
|
||||
EventStatChangeTuple msgCreateChange{StatChangeStep::kMessageCreate,
|
||||
StatResult::kDevnoInodeMismatch};
|
||||
|
||||
XCTAssertEqual(metrics->event_counts_cache_[ecExec], 2);
|
||||
XCTAssertEqual(metrics->event_counts_cache_[ecOpen], 1);
|
||||
XCTAssertEqual(metrics->event_times_cache_[etExec], nanos);
|
||||
XCTAssertEqual(metrics->event_times_cache_[etOpen], nanos * 2);
|
||||
XCTAssertEqual(metrics->stat_change_cache_[noChange], 1);
|
||||
XCTAssertEqual(metrics->stat_change_cache_[msgCreateChange], 1);
|
||||
}
|
||||
|
||||
- (void)testSetRateLimitingMetrics {
|
||||
@@ -376,6 +448,14 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
|
||||
id mockEventProcessingTimes = OCMClassMock([SNTMetricInt64Gauge class]);
|
||||
id mockEventCounts = OCMClassMock([SNTMetricCounter class]);
|
||||
int64_t nanos = 1234;
|
||||
es_message_t esExecMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, NULL);
|
||||
es_message_t esOpenMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, NULL);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
MessagePeer execMsg(mockESApi, &esExecMsg);
|
||||
MessagePeer openMsg(mockESApi, &esOpenMsg);
|
||||
|
||||
// Initial update will have non-zero sequence numbers, triggering drop detection
|
||||
es_message_t esMsgWithDrops = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, NULL);
|
||||
@@ -395,17 +475,16 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
|
||||
});
|
||||
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics =
|
||||
std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes, mockEventCounts,
|
||||
mockEventCounts, mockEventCounts, mockEventCounts, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes,
|
||||
mockEventCounts, mockEventCounts, mockEventCounts,
|
||||
mockEventCounts, mockEventCounts, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
EventDisposition::kProcessed, nanos);
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN,
|
||||
EventDisposition::kProcessed, nanos * 2);
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos, execMsg);
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos * 2,
|
||||
openMsg);
|
||||
metrics->UpdateEventStats(Processor::kRecorder, &esMsgWithDrops);
|
||||
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
|
||||
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
|
||||
@@ -416,6 +495,7 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 1);
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 1);
|
||||
XCTAssertEqual(metrics->stat_change_cache_.size(), 1);
|
||||
XCTAssertEqual(metrics->drop_cache_.size(), 2);
|
||||
|
||||
EventStatsTuple eventStats{Processor::kRecorder, esMsgWithDrops.event_type};
|
||||
@@ -430,10 +510,11 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
|
||||
// Expected call count is 8:
|
||||
// 2: event counts
|
||||
// 2: event times
|
||||
// 1: stat change step
|
||||
// 1: rate limit
|
||||
// 1: FAA
|
||||
// 2: drops (1 event, 1 global)
|
||||
int expectedCalls = 8;
|
||||
int expectedCalls = 9;
|
||||
for (int i = 0; i < expectedCalls; i++) {
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush");
|
||||
}
|
||||
@@ -443,6 +524,7 @@ std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^b
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->stat_change_cache_.size(), 0);
|
||||
// Note: The drop_cache_ should not be reset back to size 0. Instead, each
|
||||
// entry has the sequence number left intact, but drop counts reset to 0.
|
||||
XCTAssertEqual(metrics->drop_cache_.size(), 2);
|
||||
|
||||
89
Source/santad/ProcessTree/BUILD
Normal file
89
Source/santad/ProcessTree/BUILD
Normal file
@@ -0,0 +1,89 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "process",
|
||||
hdrs = ["process.h"],
|
||||
deps = [
|
||||
"//Source/santad/ProcessTree/annotations:annotator",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "process_tree",
|
||||
srcs = [
|
||||
"process_tree.cc",
|
||||
"process_tree_macos.mm",
|
||||
],
|
||||
hdrs = [
|
||||
"process_tree.h",
|
||||
"process_tree_macos.h",
|
||||
],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":process",
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
"//Source/santad/ProcessTree/annotations:annotator",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
proto_library(
|
||||
name = "process_tree_proto",
|
||||
srcs = ["process_tree.proto"],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "process_tree_cc_proto",
|
||||
deps = [":process_tree_proto"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTEndpointSecurityAdapter",
|
||||
srcs = ["SNTEndpointSecurityAdapter.mm"],
|
||||
hdrs = ["SNTEndpointSecurityAdapter.h"],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":process_tree",
|
||||
"//Source/santad:EndpointSecurityAPI",
|
||||
"//Source/santad:EndpointSecurityMessage",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "process_tree_test_helpers",
|
||||
srcs = ["process_tree_test_helpers.mm"],
|
||||
hdrs = ["process_tree_test_helpers.h"],
|
||||
deps = [
|
||||
":process",
|
||||
":process_tree",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "process_tree_test",
|
||||
srcs = ["process_tree_test.mm"],
|
||||
deps = [
|
||||
":process",
|
||||
":process_tree_test_helpers",
|
||||
"//Source/santad/ProcessTree/annotations:annotator",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
33
Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h
Normal file
33
Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#ifndef SANTA__SANTAD_PROCESSTREE_SNTENDPOINTSECURITYADAPTER_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_SNTENDPOINTSECURITYADAPTER_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
// Inform the tree of the ES event in msg.
|
||||
// This is idempotent on the tree, so can be called from multiple places with
|
||||
// the same msg.
|
||||
void InformFromESEvent(
|
||||
ProcessTree &tree,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg);
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
73
Source/santad/ProcessTree/SNTEndpointSecurityAdapter.mm
Normal file
73
Source/santad/ProcessTree/SNTEndpointSecurityAdapter.mm
Normal file
@@ -0,0 +1,73 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#include "Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#include "Source/santad/ProcessTree/process_tree_macos.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
void InformFromESEvent(ProcessTree &tree, const Message &msg) {
|
||||
struct Pid event_pid = PidFromAuditToken(msg->process->audit_token);
|
||||
auto proc = tree.Get(event_pid);
|
||||
|
||||
if (!proc) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi = msg.ESAPI();
|
||||
|
||||
switch (msg->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_EXEC:
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC: {
|
||||
std::vector<std::string> args;
|
||||
args.reserve(esapi->ExecArgCount(&msg->event.exec));
|
||||
for (int i = 0; i < esapi->ExecArgCount(&msg->event.exec); i++) {
|
||||
es_string_token_t arg = esapi->ExecArg(&msg->event.exec, i);
|
||||
args.push_back(std::string(arg.data, arg.length));
|
||||
}
|
||||
|
||||
es_string_token_t executable = msg->event.exec.target->executable->path;
|
||||
tree.HandleExec(
|
||||
msg->mach_time, **proc, PidFromAuditToken(msg->event.exec.target->audit_token),
|
||||
(struct Program){.executable = std::string(executable.data, executable.length),
|
||||
.arguments = args},
|
||||
(struct Cred){
|
||||
.uid = audit_token_to_euid(msg->event.exec.target->audit_token),
|
||||
.gid = audit_token_to_egid(msg->event.exec.target->audit_token),
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK: {
|
||||
tree.HandleFork(msg->mach_time, **proc,
|
||||
PidFromAuditToken(msg->event.fork.child->audit_token));
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT: tree.HandleExit(msg->mach_time, **proc); break;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
37
Source/santad/ProcessTree/annotations/BUILD
Normal file
37
Source/santad/ProcessTree/annotations/BUILD
Normal file
@@ -0,0 +1,37 @@
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "annotator",
|
||||
hdrs = ["annotator.h"],
|
||||
deps = [
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "originator",
|
||||
srcs = ["originator.cc"],
|
||||
hdrs = ["originator.h"],
|
||||
deps = [
|
||||
":annotator",
|
||||
"//Source/santad/ProcessTree:process",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "originator_test",
|
||||
srcs = ["originator_test.mm"],
|
||||
deps = [
|
||||
":originator",
|
||||
"//Source/santad/ProcessTree:process",
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
"//Source/santad/ProcessTree:process_tree_test_helpers",
|
||||
],
|
||||
)
|
||||
40
Source/santad/ProcessTree/annotations/annotator.h
Normal file
40
Source/santad/ProcessTree/annotations/annotator.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#ifndef SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_BASE_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_BASE_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "Source/santad/ProcessTree/process_tree.pb.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
class ProcessTree;
|
||||
class Process;
|
||||
|
||||
class Annotator {
|
||||
public:
|
||||
virtual ~Annotator() = default;
|
||||
|
||||
virtual void AnnotateFork(ProcessTree &tree, const Process &parent,
|
||||
const Process &child) = 0;
|
||||
virtual void AnnotateExec(ProcessTree &tree, const Process &orig_process,
|
||||
const Process &new_process) = 0;
|
||||
virtual std::optional<::santa::pb::v1::process_tree::Annotations> Proto()
|
||||
const = 0;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
67
Source/santad/ProcessTree/annotations/originator.cc
Normal file
67
Source/santad/ProcessTree/annotations/originator.cc
Normal file
@@ -0,0 +1,67 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#include "Source/santad/ProcessTree/annotations/originator.h"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.pb.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
|
||||
namespace ptpb = ::santa::pb::v1::process_tree;
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
void OriginatorAnnotator::AnnotateFork(ProcessTree &tree, const Process &parent,
|
||||
const Process &child) {
|
||||
// "Base case". Propagate existing annotations down to descendants.
|
||||
if (auto annotation = tree.GetAnnotation<OriginatorAnnotator>(parent)) {
|
||||
tree.AnnotateProcess(child, std::move(*annotation));
|
||||
}
|
||||
}
|
||||
|
||||
void OriginatorAnnotator::AnnotateExec(ProcessTree &tree,
|
||||
const Process &orig_process,
|
||||
const Process &new_process) {
|
||||
static const absl::flat_hash_map<std::string, ptpb::Annotations::Originator>
|
||||
originator_programs = {
|
||||
{"/usr/bin/login",
|
||||
ptpb::Annotations::Originator::Annotations_Originator_LOGIN},
|
||||
{"/usr/sbin/cron",
|
||||
ptpb::Annotations::Originator::Annotations_Originator_CRON},
|
||||
};
|
||||
|
||||
if (auto annotation = tree.GetAnnotation<OriginatorAnnotator>(orig_process)) {
|
||||
tree.AnnotateProcess(new_process, std::move(*annotation));
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto it = originator_programs.find(new_process.program_->executable);
|
||||
it != originator_programs.end()) {
|
||||
tree.AnnotateProcess(new_process,
|
||||
std::make_shared<OriginatorAnnotator>(it->second));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<ptpb::Annotations> OriginatorAnnotator::Proto() const {
|
||||
auto annotation = ptpb::Annotations();
|
||||
annotation.set_originator(originator_);
|
||||
return annotation;
|
||||
}
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
48
Source/santad/ProcessTree/annotations/originator.h
Normal file
48
Source/santad/ProcessTree/annotations/originator.h
Normal file
@@ -0,0 +1,48 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#ifndef SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_ORIGINATOR_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_ORIGINATOR_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "Source/santad/ProcessTree/annotations/annotator.h"
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.pb.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
class OriginatorAnnotator : public Annotator {
|
||||
public:
|
||||
OriginatorAnnotator()
|
||||
: originator_(::santa::pb::v1::process_tree::Annotations::Originator::
|
||||
Annotations_Originator_UNSPECIFIED){};
|
||||
explicit OriginatorAnnotator(
|
||||
::santa::pb::v1::process_tree::Annotations::Originator originator)
|
||||
: originator_(originator){};
|
||||
|
||||
void AnnotateFork(ProcessTree &tree, const Process &parent,
|
||||
const Process &child) override;
|
||||
void AnnotateExec(ProcessTree &tree, const Process &orig_process,
|
||||
const Process &new_process) override;
|
||||
|
||||
std::optional<::santa::pb::v1::process_tree::Annotations> Proto()
|
||||
const override;
|
||||
|
||||
private:
|
||||
::santa::pb::v1::process_tree::Annotations::Originator originator_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
78
Source/santad/ProcessTree/annotations/originator_test.mm
Normal file
78
Source/santad/ProcessTree/annotations/originator_test.mm
Normal file
@@ -0,0 +1,78 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include "Source/santad/ProcessTree/annotations/originator.h"
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.pb.h"
|
||||
#include "Source/santad/ProcessTree/process_tree_test_helpers.h"
|
||||
|
||||
using namespace santa::santad::process_tree;
|
||||
namespace ptpb = ::santa::pb::v1::process_tree;
|
||||
|
||||
@interface OriginatorAnnotatorTest : XCTestCase
|
||||
@property std::shared_ptr<ProcessTreeTestPeer> tree;
|
||||
@property std::shared_ptr<const Process> initProc;
|
||||
@end
|
||||
|
||||
@implementation OriginatorAnnotatorTest
|
||||
|
||||
- (void)setUp {
|
||||
std::vector<std::unique_ptr<Annotator>> annotators;
|
||||
annotators.emplace_back(std::make_unique<OriginatorAnnotator>());
|
||||
self.tree = std::make_shared<ProcessTreeTestPeer>(std::move(annotators));
|
||||
self.initProc = self.tree->InsertInit();
|
||||
}
|
||||
|
||||
- (void)testAnnotation {
|
||||
uint64_t event_id = 1;
|
||||
const struct Cred cred = {.uid = 0, .gid = 0};
|
||||
|
||||
// PID 1.1: fork() -> PID 2.2
|
||||
const struct Pid login_pid = {.pid = 2, .pidversion = 2};
|
||||
self.tree->HandleFork(event_id++, *self.initProc, login_pid);
|
||||
|
||||
// PID 2.2: exec("/usr/bin/login") -> PID 2.3
|
||||
const struct Pid login_exec_pid = {.pid = 2, .pidversion = 3};
|
||||
const struct Program login_prog = {.executable = "/usr/bin/login", .arguments = {}};
|
||||
auto login = *self.tree->Get(login_pid);
|
||||
self.tree->HandleExec(event_id++, *login, login_exec_pid, login_prog, cred);
|
||||
|
||||
// Ensure we have an annotation on login itself...
|
||||
login = *self.tree->Get(login_exec_pid);
|
||||
auto annotation_opt = self.tree->GetAnnotation<OriginatorAnnotator>(*login);
|
||||
XCTAssertTrue(annotation_opt.has_value());
|
||||
auto proto_opt = (*annotation_opt)->Proto();
|
||||
XCTAssertTrue(proto_opt.has_value());
|
||||
XCTAssertEqual(proto_opt->originator(),
|
||||
ptpb::Annotations::Originator::Annotations_Originator_LOGIN);
|
||||
|
||||
// PID 2.3: fork() -> PID 3.3
|
||||
const struct Pid shell_pid = {.pid = 3, .pidversion = 3};
|
||||
self.tree->HandleFork(event_id++, *login, shell_pid);
|
||||
// PID 3.3: exec("/bin/zsh") -> PID 3.4
|
||||
const struct Pid shell_exec_pid = {.pid = 3, .pidversion = 4};
|
||||
const struct Program shell_prog = {.executable = "/bin/zsh", .arguments = {}};
|
||||
auto shell = *self.tree->Get(shell_pid);
|
||||
self.tree->HandleExec(event_id++, *shell, shell_exec_pid, shell_prog, cred);
|
||||
|
||||
// ... and also ensure we have the same annotation on the descendant zsh.
|
||||
shell = *self.tree->Get(shell_exec_pid);
|
||||
auto descendant_annotation_opt = self.tree->GetAnnotation<OriginatorAnnotator>(*shell);
|
||||
XCTAssertTrue(descendant_annotation_opt.has_value());
|
||||
XCTAssertEqual(*descendant_annotation_opt, *annotation_opt);
|
||||
}
|
||||
|
||||
@end
|
||||
114
Source/santad/ProcessTree/process.h
Normal file
114
Source/santad/ProcessTree/process.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#ifndef SANTA__SANTAD_PROCESSTREE_PROCESS_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_PROCESS_H
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <typeindex>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/annotations/annotator.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
struct Pid {
|
||||
pid_t pid;
|
||||
uint64_t pidversion;
|
||||
|
||||
friend bool operator==(const struct Pid &lhs, const struct Pid &rhs) {
|
||||
return lhs.pid == rhs.pid && lhs.pidversion == rhs.pidversion;
|
||||
}
|
||||
friend bool operator!=(const struct Pid &lhs, const struct Pid &rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename H>
|
||||
H AbslHashValue(H h, const struct Pid &p) {
|
||||
return H::combine(std::move(h), p.pid, p.pidversion);
|
||||
}
|
||||
|
||||
struct Cred {
|
||||
uid_t uid;
|
||||
gid_t gid;
|
||||
|
||||
friend bool operator==(const struct Cred &lhs, const struct Cred &rhs) {
|
||||
return lhs.uid == rhs.uid && lhs.gid == rhs.gid;
|
||||
}
|
||||
friend bool operator!=(const struct Cred &lhs, const struct Cred &rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
};
|
||||
|
||||
struct Program {
|
||||
std::string executable;
|
||||
std::vector<std::string> arguments;
|
||||
|
||||
friend bool operator==(const struct Program &lhs, const struct Program &rhs) {
|
||||
return lhs.executable == rhs.executable && lhs.arguments == rhs.arguments;
|
||||
}
|
||||
friend bool operator!=(const struct Program &lhs, const struct Program &rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
};
|
||||
|
||||
// Fwd decls
|
||||
class ProcessTree;
|
||||
|
||||
class Process {
|
||||
public:
|
||||
explicit Process(const Pid pid, const Cred cred,
|
||||
std::shared_ptr<const Program> program,
|
||||
std::shared_ptr<const Process> parent)
|
||||
: pid_(pid),
|
||||
effective_cred_(cred),
|
||||
program_(program),
|
||||
annotations_(),
|
||||
parent_(parent),
|
||||
refcnt_(0),
|
||||
tombstoned_(false) {}
|
||||
Process(const Process &) = default;
|
||||
Process &operator=(const Process &) = delete;
|
||||
Process(Process &&) = default;
|
||||
Process &operator=(Process &&) = delete;
|
||||
|
||||
// Const "attributes" are public
|
||||
const struct Pid pid_;
|
||||
const struct Cred effective_cred_;
|
||||
const std::shared_ptr<const Program> program_;
|
||||
|
||||
private:
|
||||
// This is not API.
|
||||
// The tree helper methods are the API, and we just happen to implement
|
||||
// annotation storage and the parent relation in memory on the process right
|
||||
// now.
|
||||
friend class ProcessTree;
|
||||
absl::flat_hash_map<std::type_index, std::shared_ptr<const Annotator>>
|
||||
annotations_;
|
||||
std::shared_ptr<const Process> parent_;
|
||||
// TODO(nickmg): atomic here breaks the build.
|
||||
int refcnt_;
|
||||
// If the process is tombstoned, the event removing it from the tree has been
|
||||
// processed, but refcnt>0 keeps it alive.
|
||||
bool tombstoned_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
316
Source/santad/ProcessTree/process_tree.cc
Normal file
316
Source/santad/ProcessTree/process_tree.cc
Normal file
@@ -0,0 +1,316 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <typeindex>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/annotations/annotator.h"
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.pb.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
void ProcessTree::BackfillInsertChildren(
|
||||
absl::flat_hash_map<pid_t, std::vector<Process>> &parent_map,
|
||||
std::shared_ptr<Process> parent, const Process &unlinked_proc) {
|
||||
auto proc = std::make_shared<Process>(
|
||||
unlinked_proc.pid_, unlinked_proc.effective_cred_,
|
||||
// Re-use shared pointers from parent if value equivalent
|
||||
(parent && *(unlinked_proc.program_) == *(parent->program_))
|
||||
? parent->program_
|
||||
: unlinked_proc.program_,
|
||||
parent);
|
||||
{
|
||||
absl::MutexLock lock(&mtx_);
|
||||
map_.emplace(unlinked_proc.pid_, proc);
|
||||
}
|
||||
|
||||
// The only case where we should not have a parent is the root processes
|
||||
// (e.g. init, kthreadd).
|
||||
if (parent) {
|
||||
for (auto &annotator : annotators_) {
|
||||
annotator->AnnotateFork(*this, *(proc->parent_), *proc);
|
||||
if (proc->program_ != proc->parent_->program_) {
|
||||
annotator->AnnotateExec(*this, *(proc->parent_), *proc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const Process &child : parent_map[unlinked_proc.pid_.pid]) {
|
||||
BackfillInsertChildren(parent_map, proc, child);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::HandleFork(uint64_t timestamp, const Process &parent,
|
||||
const Pid new_pid) {
|
||||
if (Step(timestamp)) {
|
||||
std::shared_ptr<Process> child;
|
||||
{
|
||||
absl::MutexLock lock(&mtx_);
|
||||
child = std::make_shared<Process>(new_pid, parent.effective_cred_,
|
||||
parent.program_, map_[parent.pid_]);
|
||||
map_.emplace(new_pid, child);
|
||||
}
|
||||
for (const auto &annotator : annotators_) {
|
||||
annotator->AnnotateFork(*this, parent, *child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::HandleExec(uint64_t timestamp, const Process &p,
|
||||
const Pid new_pid, const Program prog,
|
||||
const Cred c) {
|
||||
if (Step(timestamp)) {
|
||||
// TODO(nickmg): should struct pid be reworked and only pid_version be
|
||||
// passed?
|
||||
assert(new_pid.pid == p.pid_.pid);
|
||||
|
||||
auto new_proc = std::make_shared<Process>(
|
||||
new_pid, c, std::make_shared<const Program>(prog), p.parent_);
|
||||
{
|
||||
absl::MutexLock lock(&mtx_);
|
||||
remove_at_.push_back({timestamp, p.pid_});
|
||||
map_.emplace(new_proc->pid_, new_proc);
|
||||
}
|
||||
for (const auto &annotator : annotators_) {
|
||||
annotator->AnnotateExec(*this, p, *new_proc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::HandleExit(uint64_t timestamp, const Process &p) {
|
||||
if (Step(timestamp)) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
remove_at_.push_back({timestamp, p.pid_});
|
||||
}
|
||||
}
|
||||
|
||||
bool ProcessTree::Step(uint64_t timestamp) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
uint64_t new_cutoff = seen_timestamps_.front();
|
||||
if (timestamp < new_cutoff) {
|
||||
// Event timestamp is before the rolling list of seen events.
|
||||
// This event may or may not have been processed, but be conservative and
|
||||
// do not reprocess.
|
||||
return false;
|
||||
}
|
||||
|
||||
// seen_timestamps_ is sorted, so only look for the value if it's possibly
|
||||
// within the array.
|
||||
if (timestamp < seen_timestamps_.back()) {
|
||||
// TODO(nickmg): If array is made bigger, replace with a binary search.
|
||||
for (const auto seen_ts : seen_timestamps_) {
|
||||
if (seen_ts == timestamp) {
|
||||
// Event seen, signal it should not be reprocessed.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto insert_point =
|
||||
std::find_if(seen_timestamps_.rbegin(), seen_timestamps_.rend(),
|
||||
[&](uint64_t x) { return x < timestamp; });
|
||||
std::move(seen_timestamps_.begin() + 1, insert_point.base(),
|
||||
seen_timestamps_.begin());
|
||||
*insert_point = timestamp;
|
||||
|
||||
for (auto it = remove_at_.begin(); it != remove_at_.end();) {
|
||||
if (it->first < new_cutoff) {
|
||||
if (auto target = GetLocked(it->second);
|
||||
target && (*target)->refcnt_ > 0) {
|
||||
(*target)->tombstoned_ = true;
|
||||
} else {
|
||||
map_.erase(it->second);
|
||||
}
|
||||
it = remove_at_.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProcessTree::RetainProcess(std::vector<struct Pid> &pids) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
for (const struct Pid &p : pids) {
|
||||
auto proc = GetLocked(p);
|
||||
if (proc) {
|
||||
(*proc)->refcnt_++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::ReleaseProcess(std::vector<struct Pid> &pids) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
for (const struct Pid &p : pids) {
|
||||
auto proc = GetLocked(p);
|
||||
if (proc) {
|
||||
if (--(*proc)->refcnt_ == 0 && (*proc)->tombstoned_) {
|
||||
map_.erase(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
---
|
||||
Annotation get/set
|
||||
---
|
||||
*/
|
||||
|
||||
void ProcessTree::AnnotateProcess(const Process &p,
|
||||
std::shared_ptr<const Annotator> a) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
const Annotator &x = *a;
|
||||
map_[p.pid_]->annotations_.emplace(std::type_index(typeid(x)), std::move(a));
|
||||
}
|
||||
|
||||
std::optional<::santa::pb::v1::process_tree::Annotations>
|
||||
ProcessTree::ExportAnnotations(const Pid p) {
|
||||
auto proc = Get(p);
|
||||
if (!proc || (*proc)->annotations_.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
::santa::pb::v1::process_tree::Annotations a;
|
||||
for (const auto &[_, annotation] : (*proc)->annotations_) {
|
||||
if (auto x = annotation->Proto(); x) a.MergeFrom(*x);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/*
|
||||
---
|
||||
Tree inspection methods
|
||||
---
|
||||
*/
|
||||
|
||||
std::vector<std::shared_ptr<const Process>> ProcessTree::RootSlice(
|
||||
std::shared_ptr<const Process> p) const {
|
||||
std::vector<std::shared_ptr<const Process>> slice;
|
||||
while (p) {
|
||||
slice.push_back(p);
|
||||
p = p->parent_;
|
||||
}
|
||||
return slice;
|
||||
}
|
||||
|
||||
void ProcessTree::Iterate(
|
||||
std::function<void(std::shared_ptr<const Process> p)> f) const {
|
||||
std::vector<std::shared_ptr<const Process>> procs;
|
||||
{
|
||||
absl::ReaderMutexLock lock(&mtx_);
|
||||
procs.reserve(map_.size());
|
||||
for (auto &[_, proc] : map_) {
|
||||
procs.push_back(proc);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &p : procs) {
|
||||
f(p);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<const Process>> ProcessTree::Get(
|
||||
const Pid target) const {
|
||||
absl::ReaderMutexLock lock(&mtx_);
|
||||
return GetLocked(target);
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<Process>> ProcessTree::GetLocked(
|
||||
const Pid target) const {
|
||||
auto it = map_.find(target);
|
||||
if (it == map_.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::shared_ptr<const Process> ProcessTree::GetParent(const Process &p) const {
|
||||
return p.parent_;
|
||||
}
|
||||
|
||||
#if SANTA_PROCESS_TREE_DEBUG
|
||||
void ProcessTree::DebugDump(std::ostream &stream) const {
|
||||
absl::ReaderMutexLock lock(&mtx_);
|
||||
stream << map_.size() << " processes" << std::endl;
|
||||
DebugDumpLocked(stream, 0, 0);
|
||||
}
|
||||
|
||||
void ProcessTree::DebugDumpLocked(std::ostream &stream, int depth,
|
||||
pid_t ppid) const
|
||||
ABSL_SHARED_LOCKS_REQUIRED(mtx_) {
|
||||
for (auto &[_, process] : map_) {
|
||||
if ((ppid == 0 && !process->parent_) ||
|
||||
(process->parent_ && process->parent_->pid_.pid == ppid)) {
|
||||
stream << std::string(2 * depth, ' ') << process->pid_.pid
|
||||
<< process->program_->executable << std::endl;
|
||||
DebugDumpLocked(stream, depth + 1, process->pid_.pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
absl::StatusOr<std::shared_ptr<ProcessTree>> CreateTree(
|
||||
std::vector<std::unique_ptr<Annotator>> annotations) {
|
||||
absl::flat_hash_set<std::type_index> seen;
|
||||
for (const auto &annotator : annotations) {
|
||||
if (seen.count(std::type_index(typeid(annotator)))) {
|
||||
return absl::InvalidArgumentError(
|
||||
"Multiple annotators of the same class");
|
||||
}
|
||||
seen.emplace(std::type_index(typeid(annotator)));
|
||||
}
|
||||
|
||||
if (seen.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto tree = std::make_shared<ProcessTree>(std::move(annotations));
|
||||
if (auto status = tree->Backfill(); !status.ok()) {
|
||||
return status;
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/*
|
||||
----
|
||||
Tokens
|
||||
----
|
||||
*/
|
||||
|
||||
ProcessToken::ProcessToken(std::shared_ptr<ProcessTree> tree,
|
||||
std::vector<struct Pid> pids)
|
||||
: tree_(std::move(tree)), pids_(std::move(pids)) {
|
||||
tree_->RetainProcess(pids);
|
||||
}
|
||||
|
||||
ProcessToken::~ProcessToken() { tree_->ReleaseProcess(pids_); }
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
189
Source/santad/ProcessTree/process_tree.h
Normal file
189
Source/santad/ProcessTree/process_tree.h
Normal file
@@ -0,0 +1,189 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#ifndef SANTA__SANTAD_PROCESSTREE_TREE_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_TREE_H
|
||||
|
||||
#include <memory>
|
||||
#include <typeinfo>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
absl::StatusOr<Process> LoadPID(pid_t pid);
|
||||
|
||||
// Fwd decl for test peer.
|
||||
class ProcessTreeTestPeer;
|
||||
|
||||
class ProcessTree {
|
||||
public:
|
||||
explicit ProcessTree(std::vector<std::unique_ptr<Annotator>> &&annotators)
|
||||
: annotators_(std::move(annotators)), seen_timestamps_({}) {}
|
||||
ProcessTree(const ProcessTree &) = delete;
|
||||
ProcessTree &operator=(const ProcessTree &) = delete;
|
||||
ProcessTree(ProcessTree &&) = delete;
|
||||
ProcessTree &operator=(ProcessTree &&) = delete;
|
||||
|
||||
// Initialize the tree with the processes currently running on the system.
|
||||
absl::Status Backfill();
|
||||
|
||||
// Inform the tree of a fork event, in which the parent process spawns a child
|
||||
// with the only difference between the two being the pid.
|
||||
void HandleFork(uint64_t timestamp, const Process &parent,
|
||||
struct Pid new_pid);
|
||||
|
||||
// Inform the tree of an exec event, in which the program and potentially cred
|
||||
// of a Process change.
|
||||
// p is the process performing the exec (running the "old" program),
|
||||
// and new_pid, prog, and cred are the new pid, program, and credentials
|
||||
// after the exec.
|
||||
// N.B. new_pid is required as the "pid version" will have changed.
|
||||
// It is a programming error to pass a new_pid such that
|
||||
// p.pid_.pid != new_pid.pid.
|
||||
void HandleExec(uint64_t timestamp, const Process &p, struct Pid new_pid,
|
||||
struct Program prog, struct Cred c);
|
||||
|
||||
// Inform the tree of a process exit.
|
||||
void HandleExit(uint64_t timestamp, const Process &p);
|
||||
|
||||
// Mark the given pids as needing to be retained in the tree's map for future
|
||||
// access. Normally, Processes are removed once all clients process past the
|
||||
// event which would remove the Process (e.g. exit), however in cases where
|
||||
// async processing occurs, the Process may need to be accessed after the
|
||||
// exit.
|
||||
void RetainProcess(std::vector<struct Pid> &pids);
|
||||
|
||||
// Release previously retained processes, signaling that the client is done
|
||||
// processing the event that retained them.
|
||||
void ReleaseProcess(std::vector<struct Pid> &pids);
|
||||
|
||||
// Annotate the given process with an Annotator (state).
|
||||
void AnnotateProcess(const Process &p, std::shared_ptr<const Annotator> a);
|
||||
|
||||
// Get the given annotation on the given process if it exists, or nullopt if
|
||||
// the annotation is not set.
|
||||
template <typename T>
|
||||
std::optional<std::shared_ptr<const T>> GetAnnotation(const Process &p) const;
|
||||
|
||||
// Get the fully merged proto form of all annotations on the given process.
|
||||
std::optional<::santa::pb::v1::process_tree::Annotations> ExportAnnotations(
|
||||
struct Pid p);
|
||||
|
||||
// Atomically get the slice of Processes going from the given process "up"
|
||||
// to the root. The root process has no parent. N.B. There may be more than
|
||||
// one root process. E.g. on Linux, both init (PID 1) and kthread (PID 2)
|
||||
// are considered roots, as they are reported to have PPID=0.
|
||||
std::vector<std::shared_ptr<const Process>> RootSlice(
|
||||
std::shared_ptr<const Process> p) const;
|
||||
|
||||
// Call f for all processes in the tree. The list of processes is captured
|
||||
// before invoking f, so it is safe to mutate the tree in f.
|
||||
void Iterate(std::function<void(std::shared_ptr<const Process>)> f) const;
|
||||
|
||||
// Get the Process for the given pid in the tree if it exists.
|
||||
std::optional<std::shared_ptr<const Process>> Get(struct Pid target) const;
|
||||
|
||||
// Traverse the tree from the given Process to its parent.
|
||||
std::shared_ptr<const Process> GetParent(const Process &p) const;
|
||||
|
||||
#if SANTA_PROCESS_TREE_DEBUG
|
||||
// Dump the tree in a human readable form to the given ostream.
|
||||
void DebugDump(std::ostream &stream) const;
|
||||
#endif
|
||||
|
||||
private:
|
||||
friend class ProcessTreeTestPeer;
|
||||
void BackfillInsertChildren(
|
||||
absl::flat_hash_map<pid_t, std::vector<Process>> &parent_map,
|
||||
std::shared_ptr<Process> parent, const Process &unlinked_proc);
|
||||
|
||||
// Mark that an event with the given timestamp is being processed.
|
||||
// Returns whether the given timestamp is "novel", and the tree should be
|
||||
// updated with the results of the event.
|
||||
bool Step(uint64_t timestamp);
|
||||
|
||||
std::optional<std::shared_ptr<Process>> GetLocked(struct Pid target) const
|
||||
ABSL_SHARED_LOCKS_REQUIRED(mtx_);
|
||||
|
||||
void DebugDumpLocked(std::ostream &stream, int depth, pid_t ppid) const;
|
||||
|
||||
std::vector<std::unique_ptr<Annotator>> annotators_;
|
||||
|
||||
mutable absl::Mutex mtx_;
|
||||
absl::flat_hash_map<const struct Pid, std::shared_ptr<Process>> map_
|
||||
ABSL_GUARDED_BY(mtx_);
|
||||
// List of pids which should be removed from map_, and at the timestamp at
|
||||
// which they should be.
|
||||
// Elements are removed when the timestamp falls out of the seen_timestamps_
|
||||
// list below, signifying that all clients have synced past the timestamp.
|
||||
std::vector<std::pair<uint64_t, struct Pid>> remove_at_ ABSL_GUARDED_BY(mtx_);
|
||||
// Rolling list of event timestamps processed by the tree.
|
||||
// This is used to ensure an event only gets processed once, even if events
|
||||
// come out of order.
|
||||
std::array<uint64_t, 32> seen_timestamps_ ABSL_GUARDED_BY(mtx_);
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
std::optional<std::shared_ptr<const T>> ProcessTree::GetAnnotation(
|
||||
const Process &p) const {
|
||||
auto it = p.annotations_.find(std::type_index(typeid(T)));
|
||||
if (it == p.annotations_.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::dynamic_pointer_cast<const T>(it->second);
|
||||
}
|
||||
|
||||
// Create a new tree, ensuring the provided annotations are valid and that
|
||||
// backfill is successful.
|
||||
absl::StatusOr<std::shared_ptr<ProcessTree>> CreateTree(
|
||||
std::vector<std::unique_ptr<Annotator>> annotations);
|
||||
|
||||
// ProcessTokens provide a lifetime based approach to retaining processes
|
||||
// in a ProcessTree. When a token is created with a list of pids that may need
|
||||
// to be referenced during processing of a given event, the ProcessToken informs
|
||||
// the tree to retain those pids in its map so any call to ProcessTree::Get()
|
||||
// during event processing succeeds. When the token is destroyed, it signals the
|
||||
// tree to release the pids, which removes them from the tree if they would have
|
||||
// fallen out otherwise due to a destruction event (e.g. exit).
|
||||
class ProcessToken {
|
||||
public:
|
||||
explicit ProcessToken(std::shared_ptr<ProcessTree> tree,
|
||||
std::vector<struct Pid> pids);
|
||||
~ProcessToken();
|
||||
ProcessToken(const ProcessToken &other)
|
||||
: ProcessToken(other.tree_, other.pids_) {}
|
||||
ProcessToken(ProcessToken &&other) noexcept
|
||||
: tree_(std::move(other.tree_)), pids_(std::move(other.pids_)) {}
|
||||
ProcessToken &operator=(const ProcessToken &other) {
|
||||
return *this = ProcessToken(other.tree_, other.pids_);
|
||||
}
|
||||
ProcessToken &operator=(ProcessToken &&other) noexcept {
|
||||
tree_ = std::move(other.tree_);
|
||||
pids_ = std::move(other.pids_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<ProcessTree> tree_;
|
||||
std::vector<struct Pid> pids_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
13
Source/santad/ProcessTree/process_tree.proto
Normal file
13
Source/santad/ProcessTree/process_tree.proto
Normal file
@@ -0,0 +1,13 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package santa.pb.v1.process_tree;
|
||||
|
||||
message Annotations {
|
||||
enum Originator {
|
||||
UNSPECIFIED = 0;
|
||||
LOGIN = 1;
|
||||
CRON = 2;
|
||||
}
|
||||
|
||||
Originator originator = 1;
|
||||
}
|
||||
26
Source/santad/ProcessTree/process_tree_macos.h
Normal file
26
Source/santad/ProcessTree/process_tree_macos.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#ifndef SANTA__SANTAD_PROCESSTREE_TREE_MACOS_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_TREE_MACOS_H
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
// Create a struct pid from the given audit token.
|
||||
struct Pid PidFromAuditToken(const audit_token_t &tok);
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
185
Source/santad/ProcessTree/process_tree_macos.mm
Normal file
185
Source/santad/ProcessTree/process_tree_macos.mm
Normal file
@@ -0,0 +1,185 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libproc.h>
|
||||
#include <mach/message.h>
|
||||
#include <string.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
namespace {
|
||||
// Modified from
|
||||
// https://chromium.googlesource.com/crashpad/crashpad/+/360e441c53ab4191a6fd2472cc57c3343a2f6944/util/posix/process_util_mac.cc
|
||||
// TODO: https://github.com/apple-oss-distributions/adv_cmds/blob/main/ps/ps.c
|
||||
absl::StatusOr<std::vector<std::string>> ProcessArgumentsForPID(pid_t pid) {
|
||||
// The format of KERN_PROCARGS2 is explained in 10.9.2 adv_cmds-153/ps/print.c
|
||||
// getproclline(). It is an int (argc) followed by the executable’s string
|
||||
// area. The string area consists of NUL-terminated strings, beginning with
|
||||
// the executable path, and then starting on an aligned boundary, all of the
|
||||
// elements of argv, envp, and applev.
|
||||
// It is possible for a process to exec() in between the two sysctl() calls
|
||||
// below. If that happens, and the string area of the new program is larger
|
||||
// than that of the old one, args_size_estimate will be too small. To detect
|
||||
// this situation, the second sysctl() attempts to fetch args_size_estimate +
|
||||
// 1 bytes, expecting to only receive args_size_estimate. If it gets the extra
|
||||
// byte, it indicates that the string area has grown, and the sysctl() pair
|
||||
// will be retried a limited number of times.
|
||||
size_t args_size_estimate;
|
||||
size_t args_size;
|
||||
std::string args;
|
||||
int tries = 3;
|
||||
do {
|
||||
int mib[] = {CTL_KERN, KERN_PROCARGS2, pid};
|
||||
int rv = sysctl(mib, 3, nullptr, &args_size_estimate, nullptr, 0);
|
||||
if (rv != 0) {
|
||||
return absl::InternalError("KERN_PROCARGS2");
|
||||
}
|
||||
args_size = args_size_estimate + 1;
|
||||
args.resize(args_size);
|
||||
rv = sysctl(mib, 3, &args[0], &args_size, nullptr, 0);
|
||||
if (rv != 0) {
|
||||
return absl::InternalError("KERN_PROCARGS2");
|
||||
}
|
||||
} while (args_size == args_size_estimate + 1 && tries--);
|
||||
if (args_size == args_size_estimate + 1) {
|
||||
return absl::InternalError("Couldn't determine size");
|
||||
}
|
||||
// KERN_PROCARGS2 needs to at least contain argc.
|
||||
if (args_size < sizeof(int)) {
|
||||
return absl::InternalError("Bad args_size");
|
||||
}
|
||||
args.resize(args_size);
|
||||
// Get argc.
|
||||
int argc;
|
||||
memcpy(&argc, &args[0], sizeof(argc));
|
||||
// Find the end of the executable path.
|
||||
size_t start_pos = sizeof(argc);
|
||||
size_t nul_pos = args.find('\0', start_pos);
|
||||
if (nul_pos == std::string::npos) {
|
||||
return absl::InternalError("Can't find end of executable path");
|
||||
}
|
||||
// Find the beginning of the string area.
|
||||
start_pos = args.find_first_not_of('\0', nul_pos);
|
||||
if (start_pos == std::string::npos) {
|
||||
return absl::InternalError("Can't find args after executable path");
|
||||
}
|
||||
std::vector<std::string> local_argv;
|
||||
while (argc-- && nul_pos != std::string::npos) {
|
||||
nul_pos = args.find('\0', start_pos);
|
||||
local_argv.push_back(args.substr(start_pos, nul_pos - start_pos));
|
||||
start_pos = nul_pos + 1;
|
||||
}
|
||||
return local_argv;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
struct Pid PidFromAuditToken(const audit_token_t &tok) {
|
||||
return (struct Pid){.pid = audit_token_to_pid(tok),
|
||||
.pidversion = (uint64_t)audit_token_to_pidversion(tok)};
|
||||
}
|
||||
|
||||
absl::StatusOr<Process> LoadPID(pid_t pid) {
|
||||
task_name_t task;
|
||||
mach_msg_type_number_t size = TASK_AUDIT_TOKEN_COUNT;
|
||||
audit_token_t token;
|
||||
|
||||
if (task_name_for_pid(mach_task_self(), pid, &task) != KERN_SUCCESS) {
|
||||
return absl::InternalError("task_name_for_pid");
|
||||
}
|
||||
|
||||
if (task_info(task, TASK_AUDIT_TOKEN, (task_info_t)&token, &size) != KERN_SUCCESS) {
|
||||
return absl::InternalError("task_info(TASK_AUDIT_TOKEN)");
|
||||
}
|
||||
mach_port_deallocate(mach_task_self(), task);
|
||||
|
||||
char path[PROC_PIDPATHINFO_MAXSIZE];
|
||||
if (proc_pidpath_audittoken(&token, path, sizeof(path)) <= 0) {
|
||||
return absl::InternalError("proc_pidpath_audittoken");
|
||||
}
|
||||
|
||||
// Don't fail Process creation if args can't be recovered.
|
||||
std::vector<std::string> args =
|
||||
ProcessArgumentsForPID(audit_token_to_pid(token)).value_or(std::vector<std::string>());
|
||||
|
||||
return Process((struct Pid){.pid = audit_token_to_pid(token),
|
||||
.pidversion = (uint64_t)audit_token_to_pidversion(token)},
|
||||
(struct Cred){
|
||||
.uid = audit_token_to_euid(token),
|
||||
.gid = audit_token_to_egid(token),
|
||||
},
|
||||
std::make_shared<struct Program>((struct Program){
|
||||
.executable = path,
|
||||
.arguments = args,
|
||||
}),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
absl::Status ProcessTree::Backfill() {
|
||||
int n_procs = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
|
||||
if (n_procs < 0) {
|
||||
return absl::InternalError("proc_listpids failed");
|
||||
}
|
||||
n_procs /= sizeof(pid_t);
|
||||
|
||||
std::vector<pid_t> pids;
|
||||
pids.resize(n_procs + 16); // add space for a few more processes
|
||||
// in case some spawn in-between.
|
||||
|
||||
n_procs = proc_listpids(PROC_ALL_PIDS, 0, pids.data(), (int)(pids.size() * sizeof(pid_t)));
|
||||
if (n_procs < 0) {
|
||||
return absl::InternalError("proc_listpids failed");
|
||||
}
|
||||
n_procs /= sizeof(pid_t);
|
||||
pids.resize(n_procs);
|
||||
|
||||
absl::flat_hash_map<pid_t, std::vector<Process>> parent_map;
|
||||
for (pid_t pid : pids) {
|
||||
auto proc_status = LoadPID(pid);
|
||||
if (proc_status.ok()) {
|
||||
auto unlinked_proc = proc_status.value();
|
||||
|
||||
// Determine ppid
|
||||
// Alternatively, there's a sysctl interface:
|
||||
// https://chromium.googlesource.com/chromium/chromium/+/master/base/process_util_openbsd.cc#32
|
||||
struct proc_bsdinfo bsdinfo;
|
||||
if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &bsdinfo, sizeof(bsdinfo)) !=
|
||||
PROC_PIDTBSDINFO_SIZE) {
|
||||
continue;
|
||||
};
|
||||
|
||||
parent_map[bsdinfo.pbi_ppid].push_back(unlinked_proc);
|
||||
}
|
||||
}
|
||||
|
||||
auto &roots = parent_map[0];
|
||||
for (const Process &p : roots) {
|
||||
BackfillInsertChildren(parent_map, std::shared_ptr<Process>(), p);
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
246
Source/santad/ProcessTree/process_tree_test.mm
Normal file
246
Source/santad/ProcessTree/process_tree_test.mm
Normal file
@@ -0,0 +1,246 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Source/santad/ProcessTree/annotations/annotator.h"
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree_test_helpers.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
namespace ptpb = ::santa::pb::v1::process_tree;
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
static constexpr std::string_view kAnnotatedExecutable = "/usr/bin/login";
|
||||
|
||||
class TestAnnotator : public Annotator {
|
||||
public:
|
||||
TestAnnotator() {}
|
||||
void AnnotateFork(ProcessTree &tree, const Process &parent, const Process &child) override;
|
||||
void AnnotateExec(ProcessTree &tree, const Process &orig_process,
|
||||
const Process &new_process) override;
|
||||
std::optional<::ptpb::Annotations> Proto() const override;
|
||||
};
|
||||
|
||||
void TestAnnotator::AnnotateFork(ProcessTree &tree, const Process &parent, const Process &child) {
|
||||
// "Base case". Propagate existing annotations down to descendants.
|
||||
if (auto annotation = tree.GetAnnotation<TestAnnotator>(parent)) {
|
||||
tree.AnnotateProcess(child, std::move(*annotation));
|
||||
}
|
||||
}
|
||||
|
||||
void TestAnnotator::AnnotateExec(ProcessTree &tree, const Process &orig_process,
|
||||
const Process &new_process) {
|
||||
if (auto annotation = tree.GetAnnotation<TestAnnotator>(orig_process)) {
|
||||
tree.AnnotateProcess(new_process, std::move(*annotation));
|
||||
return;
|
||||
}
|
||||
|
||||
if (new_process.program_->executable == kAnnotatedExecutable) {
|
||||
tree.AnnotateProcess(new_process, std::make_shared<TestAnnotator>());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<::ptpb::Annotations> TestAnnotator::Proto() const {
|
||||
return std::nullopt;
|
||||
}
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
using namespace santa::santad::process_tree;
|
||||
|
||||
@interface ProcessTreeTest : XCTestCase
|
||||
@property std::shared_ptr<ProcessTreeTestPeer> tree;
|
||||
@property std::shared_ptr<const Process> initProc;
|
||||
@end
|
||||
|
||||
@implementation ProcessTreeTest
|
||||
|
||||
- (void)setUp {
|
||||
std::vector<std::unique_ptr<Annotator>> annotators{};
|
||||
self.tree = std::make_shared<ProcessTreeTestPeer>(std::move(annotators));
|
||||
self.initProc = self.tree->InsertInit();
|
||||
}
|
||||
|
||||
- (void)testSimpleOps {
|
||||
uint64_t event_id = 1;
|
||||
// PID 1.1: fork() -> PID 2.2
|
||||
const struct Pid child_pid = {.pid = 2, .pidversion = 2};
|
||||
self.tree->HandleFork(event_id++, *self.initProc, child_pid);
|
||||
|
||||
auto child_opt = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child_opt.has_value());
|
||||
std::shared_ptr<const Process> child = *child_opt;
|
||||
XCTAssertEqual(child->pid_, child_pid);
|
||||
XCTAssertEqual(child->program_, self.initProc->program_);
|
||||
XCTAssertEqual(child->effective_cred_, self.initProc->effective_cred_);
|
||||
XCTAssertEqual(self.tree->GetParent(*child), self.initProc);
|
||||
|
||||
// PID 2.2: exec("/bin/bash") -> PID 2.3
|
||||
const struct Pid child_exec_pid = {.pid = 2, .pidversion = 3};
|
||||
const struct Program child_exec_prog = {.executable = "/bin/bash",
|
||||
.arguments = {"/bin/bash", "-i"}};
|
||||
self.tree->HandleExec(event_id++, *child, child_exec_pid, child_exec_prog,
|
||||
child->effective_cred_);
|
||||
|
||||
child_opt = self.tree->Get(child_exec_pid);
|
||||
XCTAssertTrue(child_opt.has_value());
|
||||
child = *child_opt;
|
||||
XCTAssertEqual(child->pid_, child_exec_pid);
|
||||
XCTAssertEqual(*child->program_, child_exec_prog);
|
||||
XCTAssertEqual(child->effective_cred_, self.initProc->effective_cred_);
|
||||
}
|
||||
|
||||
// We can't test the full backfill process, as retrieving information on
|
||||
// processes (with task_name_for_pid) requires privileges.
|
||||
// Test what we can by LoadPID'ing ourselves.
|
||||
- (void)testLoadPID {
|
||||
auto proc = LoadPID(getpid()).value();
|
||||
|
||||
audit_token_t self_tok;
|
||||
mach_msg_type_number_t count = TASK_AUDIT_TOKEN_COUNT;
|
||||
XCTAssertEqual(task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)&self_tok, &count),
|
||||
KERN_SUCCESS);
|
||||
|
||||
XCTAssertEqual(proc.pid_.pid, audit_token_to_pid(self_tok));
|
||||
XCTAssertEqual(proc.pid_.pidversion, audit_token_to_pidversion(self_tok));
|
||||
|
||||
XCTAssertEqual(proc.effective_cred_.uid, geteuid());
|
||||
XCTAssertEqual(proc.effective_cred_.gid, getegid());
|
||||
|
||||
[[[NSProcessInfo processInfo] arguments]
|
||||
enumerateObjectsUsingBlock:^(NSString *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
|
||||
XCTAssertEqualObjects(@(proc.program_->arguments[idx].c_str()), obj);
|
||||
if (idx == 0) {
|
||||
XCTAssertEqualObjects(@(proc.program_->executable.c_str()), obj);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testAnnotation {
|
||||
std::vector<std::unique_ptr<Annotator>> annotators{};
|
||||
annotators.emplace_back(std::make_unique<TestAnnotator>());
|
||||
self.tree = std::make_shared<ProcessTreeTestPeer>(std::move(annotators));
|
||||
self.initProc = self.tree->InsertInit();
|
||||
|
||||
uint64_t event_id = 1;
|
||||
const struct Cred cred = {.uid = 0, .gid = 0};
|
||||
|
||||
// PID 1.1: fork() -> PID 2.2
|
||||
const struct Pid login_pid = {.pid = 2, .pidversion = 2};
|
||||
self.tree->HandleFork(event_id++, *self.initProc, login_pid);
|
||||
|
||||
// PID 2.2: exec("/usr/bin/login") -> PID 2.3
|
||||
const struct Pid login_exec_pid = {.pid = 2, .pidversion = 3};
|
||||
const struct Program login_prog = {.executable = std::string(kAnnotatedExecutable),
|
||||
.arguments = {}};
|
||||
auto login = *self.tree->Get(login_pid);
|
||||
self.tree->HandleExec(event_id++, *login, login_exec_pid, login_prog, cred);
|
||||
|
||||
// Ensure we have an annotation on login itself...
|
||||
login = *self.tree->Get(login_exec_pid);
|
||||
auto annotation = self.tree->GetAnnotation<TestAnnotator>(*login);
|
||||
XCTAssertTrue(annotation.has_value());
|
||||
|
||||
// PID 2.3: fork() -> PID 3.3
|
||||
const struct Pid shell_pid = {.pid = 3, .pidversion = 3};
|
||||
self.tree->HandleFork(event_id++, *login, shell_pid);
|
||||
// PID 3.3: exec("/bin/zsh") -> PID 3.4
|
||||
const struct Pid shell_exec_pid = {.pid = 3, .pidversion = 4};
|
||||
const struct Program shell_prog = {.executable = "/bin/zsh", .arguments = {}};
|
||||
auto shell = *self.tree->Get(shell_pid);
|
||||
self.tree->HandleExec(event_id++, *shell, shell_exec_pid, shell_prog, cred);
|
||||
|
||||
// ... and also ensure we have an annotation on the descendant zsh.
|
||||
shell = *self.tree->Get(shell_exec_pid);
|
||||
annotation = self.tree->GetAnnotation<TestAnnotator>(*shell);
|
||||
XCTAssertTrue(annotation.has_value());
|
||||
}
|
||||
|
||||
- (void)testCleanup {
|
||||
uint64_t event_id = 1;
|
||||
const struct Pid child_pid = {.pid = 2, .pidversion = 2};
|
||||
{
|
||||
self.tree->HandleFork(event_id++, *self.initProc, child_pid);
|
||||
auto child = *self.tree->Get(child_pid);
|
||||
self.tree->HandleExit(event_id++, *child);
|
||||
}
|
||||
|
||||
// We should still be able to get a handle to child...
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
}
|
||||
|
||||
// ... until we step far enough into the future (32 events).
|
||||
struct Pid churn_pid = {.pid = 3, .pidversion = 3};
|
||||
for (int i = 0; i < 32; i++) {
|
||||
self.tree->HandleFork(event_id++, *self.initProc, churn_pid);
|
||||
churn_pid.pid++;
|
||||
}
|
||||
|
||||
// Now when we try processing the next event, it should have fallen out of the tree.
|
||||
self.tree->HandleFork(event_id++, *self.initProc, churn_pid);
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertFalse(child.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testRefcountCleanup {
|
||||
uint64_t event_id = 1;
|
||||
const struct Pid child_pid = {.pid = 2, .pidversion = 2};
|
||||
{
|
||||
self.tree->HandleFork(event_id++, *self.initProc, child_pid);
|
||||
auto child = *self.tree->Get(child_pid);
|
||||
self.tree->HandleExit(event_id++, *child);
|
||||
}
|
||||
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
std::vector<struct Pid> pids = {(*child)->pid_};
|
||||
self.tree->RetainProcess(pids);
|
||||
}
|
||||
|
||||
// Even if we step far into the future, we should still be able to lookup
|
||||
// the child.
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
struct Pid churn_pid = {.pid = 100 + i, .pidversion = (uint64_t)(100 + i)};
|
||||
self.tree->HandleFork(event_id++, *self.initProc, churn_pid);
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
}
|
||||
|
||||
// But when released...
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
std::vector<struct Pid> pids = {(*child)->pid_};
|
||||
self.tree->ReleaseProcess(pids);
|
||||
}
|
||||
|
||||
// ... it should immediately be removed.
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertFalse(child.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
32
Source/santad/ProcessTree/process_tree_test_helpers.h
Normal file
32
Source/santad/ProcessTree/process_tree_test_helpers.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#ifndef SANTA__SANTAD_PROCESSTREE_TREE_TEST_HELPERS_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_TREE_TEST_HELPERS_H
|
||||
#include <memory>
|
||||
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
class ProcessTreeTestPeer : public ProcessTree {
|
||||
public:
|
||||
explicit ProcessTreeTestPeer(
|
||||
std::vector<std::unique_ptr<Annotator>> &&annotators)
|
||||
: ProcessTree(std::move(annotators)) {}
|
||||
std::shared_ptr<const Process> InsertInit();
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
42
Source/santad/ProcessTree/process_tree_test_helpers.mm
Normal file
42
Source/santad/ProcessTree/process_tree_test_helpers.mm
Normal file
@@ -0,0 +1,42 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
class ProcessTreeTestPeer : public ProcessTree {
|
||||
public:
|
||||
std::shared_ptr<const Process> InsertInit();
|
||||
};
|
||||
|
||||
std::shared_ptr<const Process> ProcessTreeTestPeer::InsertInit() {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
struct Pid initpid = {
|
||||
.pid = 1,
|
||||
.pidversion = 1,
|
||||
};
|
||||
auto proc = std::make_shared<Process>(
|
||||
initpid, (Cred){.uid = 0, .gid = 0},
|
||||
std::make_shared<Program>((Program){.executable = "/init", .arguments = {"/init"}}), nullptr);
|
||||
map_.emplace(initpid, proc);
|
||||
return proc;
|
||||
}
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
@@ -65,19 +65,21 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
// Adds a fake cached decision to SNTDecisionCache for pending files. If the file
|
||||
// is executed before we can create a transitive rule for it, then we can at
|
||||
// least log the pending decision info.
|
||||
- (void)saveFakeDecision:(const es_file_t *)esFile {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] initWithEndpointSecurityFile:esFile];
|
||||
- (void)saveFakeDecision:(SNTFileInfo *)fileInfo {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] initWithVnode:fileInfo.vnode];
|
||||
cd.decision = SNTEventStateAllowPendingTransitive;
|
||||
cd.sha256 = @"pending";
|
||||
[[SNTDecisionCache sharedCache] cacheDecision:cd];
|
||||
}
|
||||
|
||||
- (void)removeFakeDecision:(const es_file_t *)esFile {
|
||||
[[SNTDecisionCache sharedCache] forgetCachedDecisionForFile:esFile->stat];
|
||||
- (void)removeFakeDecision:(SNTFileInfo *)fileInfo {
|
||||
[[SNTDecisionCache sharedCache] forgetCachedDecisionForVnode:fileInfo.vnode];
|
||||
}
|
||||
|
||||
- (BOOL)handleEvent:(const Message &)esMsg withLogger:(std::shared_ptr<Logger>)logger {
|
||||
const es_file_t *targetFile = NULL;
|
||||
SNTFileInfo *targetFile;
|
||||
NSString *targetPath;
|
||||
NSError *error;
|
||||
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE:
|
||||
@@ -90,7 +92,9 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
return NO;
|
||||
}
|
||||
|
||||
targetFile = esMsg->event.close.target;
|
||||
targetPath = @(esMsg->event.close.target->path.data);
|
||||
targetFile = [[SNTFileInfo alloc] initWithEndpointSecurityFile:esMsg->event.close.target
|
||||
error:&error];
|
||||
|
||||
break;
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME:
|
||||
@@ -105,7 +109,24 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
return NO;
|
||||
}
|
||||
|
||||
targetFile = esMsg->event.rename.source;
|
||||
targetFile = [[SNTFileInfo alloc] initWithEndpointSecurityFile:esMsg->event.rename.source
|
||||
error:&error];
|
||||
if (!targetFile) {
|
||||
LOGD(@"Unable to locate source file for rename event while creating transitive. Falling "
|
||||
@"back to destination. Path: %s, Error: %@",
|
||||
esMsg->event.rename.source->path.data, error);
|
||||
if (esMsg->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
|
||||
targetPath = @(esMsg->event.rename.destination.existing_file->path.data);
|
||||
targetFile = [[SNTFileInfo alloc]
|
||||
initWithEndpointSecurityFile:esMsg->event.rename.destination.existing_file
|
||||
error:&error];
|
||||
} else {
|
||||
targetPath = [NSString
|
||||
stringWithFormat:@"%s/%s", esMsg->event.rename.destination.new_path.dir->path.data,
|
||||
esMsg->event.rename.destination.new_path.filename.data];
|
||||
targetFile = [[SNTFileInfo alloc] initWithPath:targetPath error:&error];
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT:
|
||||
@@ -119,6 +140,9 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
[self createTransitiveRule:esMsg target:targetFile logger:logger];
|
||||
return YES;
|
||||
} else {
|
||||
LOGD(@"Unable to create SNTFileInfo while attempting to create transitive rule. Event: %d | "
|
||||
@"Path: %@ | Error: %@",
|
||||
(int)esMsg->event_type, targetPath, error);
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
@@ -127,31 +151,21 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
// compiler. It checks if the closed file is executable, and if so, transitively allowlists it.
|
||||
// The passed in message contains the pid of the writing process and path of closed file.
|
||||
- (void)createTransitiveRule:(const Message &)esMsg
|
||||
target:(const es_file_t *)targetFile
|
||||
target:(SNTFileInfo *)targetFile
|
||||
logger:(std::shared_ptr<Logger>)logger {
|
||||
NSError *error = nil;
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithEndpointSecurityFile:targetFile error:&error];
|
||||
if (error) {
|
||||
LOGD(@"Unable to create SNTFileInfo while attempting to create transitive rule. Event: %d | "
|
||||
@"Path: %@ | Error: %@",
|
||||
(int)esMsg->event_type, @(targetFile->path.data), error);
|
||||
return;
|
||||
}
|
||||
|
||||
[self saveFakeDecision:targetFile];
|
||||
|
||||
// Check if this file is an executable.
|
||||
if (fi.isExecutable) {
|
||||
if (targetFile.isExecutable) {
|
||||
// Check if there is an existing (non-transitive) rule for this file. We leave existing rules
|
||||
// alone, so that a allowlist or blocklist rule can't be overwritten by a transitive one.
|
||||
SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable];
|
||||
SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
SNTRule *prevRule = [ruleTable ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.binarySHA256 = targetFile.SHA256,
|
||||
}];
|
||||
if (!prevRule || prevRule.state == SNTRuleStateAllowTransitive) {
|
||||
// Construct a new transitive allowlist rule for the executable.
|
||||
SNTRule *rule = [[SNTRule alloc] initWithIdentifier:fi.SHA256
|
||||
SNTRule *rule = [[SNTRule alloc] initWithIdentifier:targetFile.SHA256
|
||||
state:SNTRuleStateAllowTransitive
|
||||
type:SNTRuleTypeBinary
|
||||
customMsg:@""];
|
||||
@@ -161,7 +175,7 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
if (![ruleTable addRules:@[ rule ] ruleCleanup:SNTRuleCleanupNone error:&err]) {
|
||||
LOGE(@"unable to add new transitive rule to database: %@", err.localizedDescription);
|
||||
} else {
|
||||
logger->LogAllowlist(esMsg, [fi.SHA256 UTF8String]);
|
||||
logger->LogAllowlist(esMsg, [targetFile.SHA256 UTF8String]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/SNTCompilerController.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include "Source/santad/SNTCompilerController.h"
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <string_view>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
@@ -39,10 +39,10 @@ static const pid_t PID_MAX = 99999;
|
||||
|
||||
@interface SNTCompilerController (Testing)
|
||||
- (BOOL)isCompiler:(const audit_token_t &)tok;
|
||||
- (void)saveFakeDecision:(const es_file_t *)esFile;
|
||||
- (void)removeFakeDecision:(const es_file_t *)esFile;
|
||||
- (void)saveFakeDecision:(SNTFileInfo *)esFile;
|
||||
- (void)removeFakeDecision:(SNTFileInfo *)esFile;
|
||||
- (void)createTransitiveRule:(const Message &)esMsg
|
||||
target:(const es_file_t *)targetFile
|
||||
target:(SNTFileInfo *)targetFile
|
||||
logger:(std::shared_ptr<Logger>)logger;
|
||||
@end
|
||||
|
||||
@@ -117,34 +117,39 @@ static const pid_t PID_MAX = 99999;
|
||||
}
|
||||
|
||||
- (void)testSaveFakeDecision {
|
||||
es_file_t file = MakeESFile("foo", {
|
||||
.st_dev = 12,
|
||||
.st_ino = 34,
|
||||
});
|
||||
SantaVnode vnode{
|
||||
.fsid = 12,
|
||||
.fileid = 34,
|
||||
};
|
||||
|
||||
OCMExpect([self.mockDecisionCache
|
||||
cacheDecision:[OCMArg checkWithBlock:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.vnodeId.fsid == file.stat.st_dev && cd.vnodeId.fileid == file.stat.st_ino &&
|
||||
cd.decision == SNTEventStateAllowPendingTransitive &&
|
||||
return cd.vnodeId == vnode && cd.decision == SNTEventStateAllowPendingTransitive &&
|
||||
[cd.sha256 isEqualToString:@"pending"];
|
||||
}]]);
|
||||
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMExpect([mockFileInfo vnode]).andReturn(vnode);
|
||||
|
||||
SNTCompilerController *cc = [[SNTCompilerController alloc] init];
|
||||
[cc saveFakeDecision:&file];
|
||||
[cc saveFakeDecision:mockFileInfo];
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(self.mockDecisionCache), "Unable to verify all expectations");
|
||||
}
|
||||
|
||||
- (void)testRemoveFakeDecision {
|
||||
es_file_t file = MakeESFile("foo", {
|
||||
.st_dev = 12,
|
||||
.st_ino = 34,
|
||||
});
|
||||
SantaVnode vnode{
|
||||
.fsid = 12,
|
||||
.fileid = 34,
|
||||
};
|
||||
|
||||
OCMExpect([self.mockDecisionCache forgetCachedDecisionForFile:file.stat]);
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMExpect([mockFileInfo vnode]).andReturn(vnode);
|
||||
|
||||
OCMExpect([self.mockDecisionCache forgetCachedDecisionForVnode:vnode]);
|
||||
|
||||
SNTCompilerController *cc = [[SNTCompilerController alloc] init];
|
||||
[cc removeFakeDecision:&file];
|
||||
[cc removeFakeDecision:mockFileInfo];
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(self.mockDecisionCache), "Unable to verify all expectations");
|
||||
}
|
||||
@@ -153,6 +158,7 @@ static const pid_t PID_MAX = 99999;
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_file_t ignoredFile = MakeESFile("/dev/bar");
|
||||
es_file_t normalFile = MakeESFile("bar");
|
||||
SantaVnode vnodeNormal = SantaVnode::VnodeForFile(&normalFile);
|
||||
audit_token_t compilerTok = MakeAuditToken(12, 34);
|
||||
audit_token_t notCompilerTok = MakeAuditToken(56, 78);
|
||||
es_process_t compilerProc = MakeESProcess(&file, compilerTok, {});
|
||||
@@ -221,33 +227,140 @@ static const pid_t PID_MAX = 99999;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
id mockCompilerController = OCMPartialMock(cc);
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMStub([mockFileInfo alloc]).andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo initWithEndpointSecurityFile:&normalFile error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo vnode]).andReturn(vnodeNormal);
|
||||
|
||||
OCMExpect([mockCompilerController createTransitiveRule:msg
|
||||
target:esMsg.event.close.target
|
||||
logger:nullptr])
|
||||
OCMExpect([mockCompilerController
|
||||
createTransitiveRule:msg
|
||||
target:[OCMArg checkWithBlock:^BOOL(SNTFileInfo *fi) {
|
||||
return fi.vnode.fsid == normalFile.stat.st_dev &&
|
||||
fi.vnode.fileid == normalFile.stat.st_ino;
|
||||
}]
|
||||
logger:nullptr])
|
||||
.ignoringNonObjectArgs();
|
||||
|
||||
XCTAssertTrue([cc handleEvent:msg withLogger:nullptr]);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController), "Unable to verify all expectations");
|
||||
[mockCompilerController stopMocking];
|
||||
[mockFileInfo stopMocking];
|
||||
}
|
||||
// Ensure transitive rules are created for RENAME events from the source path
|
||||
{
|
||||
esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &compilerProc);
|
||||
esMsg.event.rename.source = &normalFile;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
id mockCompilerController = OCMPartialMock(cc);
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMStub([mockFileInfo alloc]).andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo initWithEndpointSecurityFile:&normalFile error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo vnode]).andReturn(vnodeNormal);
|
||||
|
||||
OCMExpect([mockCompilerController createTransitiveRule:msg
|
||||
target:esMsg.event.close.target
|
||||
logger:nullptr])
|
||||
OCMExpect([mockCompilerController
|
||||
createTransitiveRule:msg
|
||||
target:[OCMArg checkWithBlock:^BOOL(SNTFileInfo *fi) {
|
||||
return fi.vnode.fsid == normalFile.stat.st_dev &&
|
||||
fi.vnode.fileid == normalFile.stat.st_ino;
|
||||
}]
|
||||
logger:nullptr])
|
||||
.ignoringNonObjectArgs();
|
||||
|
||||
XCTAssertTrue([cc handleEvent:msg withLogger:nullptr]);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController), "Unable to verify all expectations");
|
||||
[mockCompilerController stopMocking];
|
||||
[mockFileInfo stopMocking];
|
||||
}
|
||||
// Ensure transitive rules are created for RENAME events from the existing destinatio path as a
|
||||
// fallback
|
||||
{
|
||||
es_file_t destFile = MakeESFile("dest", MakeStat(1000));
|
||||
esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &compilerProc);
|
||||
esMsg.event.rename.source = &normalFile;
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
esMsg.event.rename.destination.existing_file = &destFile;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
SantaVnode vnodeDest = SantaVnode::VnodeForFile(&destFile);
|
||||
|
||||
id mockCompilerController = OCMPartialMock(cc);
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMStub([mockFileInfo alloc]).andReturn(mockFileInfo);
|
||||
// Return nil the first time when the source path is looked up
|
||||
OCMExpect([mockFileInfo initWithEndpointSecurityFile:&normalFile error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(nil);
|
||||
OCMExpect([mockFileInfo initWithEndpointSecurityFile:&destFile error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo vnode]).andReturn(vnodeDest);
|
||||
|
||||
OCMExpect([mockCompilerController
|
||||
createTransitiveRule:msg
|
||||
target:[OCMArg checkWithBlock:^BOOL(SNTFileInfo *fi) {
|
||||
return fi.vnode.fsid == destFile.stat.st_dev &&
|
||||
fi.vnode.fileid == destFile.stat.st_ino;
|
||||
}]
|
||||
logger:nullptr])
|
||||
.ignoringNonObjectArgs();
|
||||
|
||||
XCTAssertTrue([cc handleEvent:msg withLogger:nullptr]);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController), "Unable to verify all expectations");
|
||||
[mockCompilerController stopMocking];
|
||||
[mockFileInfo stopMocking];
|
||||
}
|
||||
// Ensure transitive rules are created for RENAME events from the existing destinatio path as a
|
||||
// fallback
|
||||
{
|
||||
es_file_t destDir = MakeESFile("/usr/bin", MakeStat(1000));
|
||||
es_string_token_t destFilename = MakeESStringToken("true");
|
||||
esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &compilerProc);
|
||||
esMsg.event.rename.source = &normalFile;
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
esMsg.event.rename.destination.new_path.dir = &destDir;
|
||||
esMsg.event.rename.destination.new_path.filename = destFilename;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
NSString *expectedTarget =
|
||||
[NSString stringWithFormat:@"%s/%s", destDir.path.data, destFilename.data];
|
||||
|
||||
struct stat sbNewFile;
|
||||
XCTAssertEqual(stat("/usr/bin/true", &sbNewFile), 0);
|
||||
SantaVnode vnodeDest = SantaVnode::VnodeForFile(sbNewFile);
|
||||
|
||||
id mockCompilerController = OCMPartialMock(cc);
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMStub([mockFileInfo alloc]).andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo vnode]).andReturn(vnodeDest);
|
||||
|
||||
// Return nil the first time when the source path is looked up
|
||||
OCMExpect([mockFileInfo initWithEndpointSecurityFile:&normalFile error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(nil);
|
||||
OCMExpect([mockFileInfo initWithPath:expectedTarget error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(mockFileInfo);
|
||||
|
||||
OCMExpect([mockCompilerController
|
||||
createTransitiveRule:msg
|
||||
target:[OCMArg checkWithBlock:^BOOL(SNTFileInfo *fi) {
|
||||
return fi.vnode.fsid == sbNewFile.st_dev &&
|
||||
fi.vnode.fileid == sbNewFile.st_ino;
|
||||
}]
|
||||
logger:nullptr])
|
||||
.ignoringNonObjectArgs();
|
||||
|
||||
XCTAssertTrue([cc handleEvent:msg withLogger:nullptr]);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController), "Unable to verify all expectations");
|
||||
[mockCompilerController stopMocking];
|
||||
[mockFileInfo stopMocking];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTStrengthify.h"
|
||||
#import "Source/common/SNTXPCNotifierInterface.h"
|
||||
@@ -97,11 +98,19 @@ double watchdogRAMPeak = 0;
|
||||
|
||||
#pragma mark Database ops
|
||||
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID, int64_t signingID))reply {
|
||||
- (void)databaseRuleCounts:(void (^)(RuleCounts ruleTypeCounts))reply {
|
||||
SNTRuleTable *rdb = [SNTDatabaseController ruleTable];
|
||||
reply([rdb binaryRuleCount], [rdb certificateRuleCount], [rdb compilerRuleCount],
|
||||
[rdb transitiveRuleCount], [rdb teamIDRuleCount], [rdb signingIDRuleCount]);
|
||||
RuleCounts ruleCounts{
|
||||
.binary = [rdb binaryRuleCount],
|
||||
.certificate = [rdb certificateRuleCount],
|
||||
.compiler = [rdb compilerRuleCount],
|
||||
.transitive = [rdb transitiveRuleCount],
|
||||
.teamID = [rdb teamIDRuleCount],
|
||||
.signingID = [rdb signingIDRuleCount],
|
||||
.cdhash = [rdb cdhashRuleCount],
|
||||
};
|
||||
|
||||
reply(ruleCounts);
|
||||
}
|
||||
|
||||
- (void)databaseRuleAddRules:(NSArray *)rules
|
||||
@@ -142,15 +151,9 @@ double watchdogRAMPeak = 0;
|
||||
[[SNTDatabaseController eventTable] deleteEventsWithIds:ids];
|
||||
}
|
||||
|
||||
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTRule *))reply {
|
||||
reply([[SNTDatabaseController ruleTable] ruleForBinarySHA256:binarySHA256
|
||||
signingID:signingID
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID]);
|
||||
- (void)databaseRuleForIdentifiers:(SNTRuleIdentifiers *)identifiers
|
||||
reply:(void (^)(SNTRule *))reply {
|
||||
reply([[SNTDatabaseController ruleTable] ruleForIdentifiers:[identifiers toStruct]]);
|
||||
}
|
||||
|
||||
- (void)staticRuleCount:(void (^)(int64_t count))reply {
|
||||
@@ -175,17 +178,9 @@ double watchdogRAMPeak = 0;
|
||||
#pragma mark Decision Ops
|
||||
|
||||
- (void)decisionForFilePath:(NSString *)filePath
|
||||
fileSHA256:(NSString *)fileSHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
identifiers:(SNTRuleIdentifiers *)identifiers
|
||||
reply:(void (^)(SNTEventState))reply {
|
||||
reply([self.policyProcessor decisionForFilePath:filePath
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID]
|
||||
.decision);
|
||||
reply([self.policyProcessor decisionForFilePath:filePath identifiers:identifiers].decision);
|
||||
}
|
||||
|
||||
#pragma mark Config Ops
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
|
||||
@interface SNTDecisionCache : NSObject
|
||||
|
||||
@@ -24,7 +25,7 @@
|
||||
|
||||
- (void)cacheDecision:(SNTCachedDecision *)cd;
|
||||
- (SNTCachedDecision *)cachedDecisionForFile:(const struct stat &)statInfo;
|
||||
- (void)forgetCachedDecisionForFile:(const struct stat &)statInfo;
|
||||
- (void)forgetCachedDecisionForVnode:(SantaVnode)vnode;
|
||||
- (SNTCachedDecision *)resetTimestampForCachedDecision:(const struct stat &)statInfo;
|
||||
|
||||
@end
|
||||
|
||||
@@ -58,8 +58,8 @@
|
||||
return self->_decisionCache.get(SantaVnode::VnodeForFile(statInfo));
|
||||
}
|
||||
|
||||
- (void)forgetCachedDecisionForFile:(const struct stat &)statInfo {
|
||||
self->_decisionCache.remove(SantaVnode::VnodeForFile(statInfo));
|
||||
- (void)forgetCachedDecisionForVnode:(SantaVnode)vnode {
|
||||
self->_decisionCache.remove(vnode);
|
||||
}
|
||||
|
||||
// Whenever a cached decision resulting from a transitive allowlist rule is used to allow the
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
@@ -70,7 +71,7 @@ SNTCachedDecision *MakeCachedDecision(struct stat sb, SNTEventState decision) {
|
||||
XCTAssertEqual(cachedCD.vnodeId.fileid, cd.vnodeId.fileid);
|
||||
|
||||
// Delete the item from the cache and ensure it no longer exists
|
||||
[dc forgetCachedDecisionForFile:sb];
|
||||
[dc forgetCachedDecisionForVnode:SantaVnode::VnodeForFile(sb)];
|
||||
XCTAssertNil([dc cachedDecisionForFile:sb]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ const static NSString *kBlockTeamID = @"BlockTeamID";
|
||||
const static NSString *kAllowTeamID = @"AllowTeamID";
|
||||
const static NSString *kBlockSigningID = @"BlockSigningID";
|
||||
const static NSString *kAllowSigningID = @"AllowSigningID";
|
||||
const static NSString *kBlockCDHash = @"BlockCDHash";
|
||||
const static NSString *kAllowCDHash = @"AllowCDHash";
|
||||
const static NSString *kBlockScope = @"BlockScope";
|
||||
const static NSString *kAllowScope = @"AllowScope";
|
||||
const static NSString *kAllowUnknown = @"AllowUnknown";
|
||||
|
||||
@@ -162,6 +162,8 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
case SNTEventStateAllowTeamID: eventTypeStr = kAllowTeamID; break;
|
||||
case SNTEventStateBlockSigningID: eventTypeStr = kBlockSigningID; break;
|
||||
case SNTEventStateAllowSigningID: eventTypeStr = kAllowSigningID; break;
|
||||
case SNTEventStateBlockCDHash: eventTypeStr = kBlockCDHash; break;
|
||||
case SNTEventStateAllowCDHash: eventTypeStr = kAllowCDHash; break;
|
||||
case SNTEventStateBlockScope: eventTypeStr = kBlockScope; break;
|
||||
case SNTEventStateAllowScope: eventTypeStr = kAllowScope; break;
|
||||
case SNTEventStateBlockUnknown: eventTypeStr = kBlockUnknown; break;
|
||||
@@ -230,7 +232,7 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithEndpointSecurityFile:targetProc->executable
|
||||
error:&fileInfoError];
|
||||
if (unlikely(!binInfo)) {
|
||||
if (config.failClosed && config.clientMode == SNTClientModeLockdown) {
|
||||
if (config.failClosed) {
|
||||
LOGE(@"Failed to read file %@: %@ and denying action", @(targetProc->executable->path.data),
|
||||
fileInfoError.localizedDescription);
|
||||
postAction(SNTActionRespondDeny);
|
||||
@@ -254,9 +256,11 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
// TODO(markowsky): Maybe add a metric here for how many large executables we're seeing.
|
||||
// if (binInfo.fileSize > SomeUpperLimit) ...
|
||||
|
||||
SNTCachedDecision *cd = [self.policyProcessor
|
||||
decisionForFileInfo:binInfo
|
||||
targetProcess:targetProc
|
||||
SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo
|
||||
targetProcess:targetProc
|
||||
preCodesignCheckCallback:^(void) {
|
||||
esMsg.UpdateStatState(StatChangeStep::kCodesignValidation);
|
||||
}
|
||||
entitlementsFilterCallback:^NSDictionary *(const char *teamID, NSDictionary *entitlements) {
|
||||
if (!entitlements) {
|
||||
return nil;
|
||||
@@ -326,6 +330,7 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
se.signingChain = cd.certChain;
|
||||
se.teamID = cd.teamID;
|
||||
se.signingID = cd.signingID;
|
||||
se.cdhash = cd.cdhash;
|
||||
se.pid = @(audit_token_to_pid(targetProc->audit_token));
|
||||
se.ppid = @(audit_token_to_pid(targetProc->parent_audit_token));
|
||||
se.parentName = @(esMsg.ParentProcessName().c_str());
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#import "Source/santad/DataLayer/SNTEventTable.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
@@ -201,6 +202,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
});
|
||||
es_process_t procExec = MakeESProcess(&fileExec);
|
||||
procExec.is_platform_binary = false;
|
||||
procExec.codesigning_flags = CS_SIGNED | CS_VALID;
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc);
|
||||
esMsg.event.exec.target = &procExec;
|
||||
|
||||
@@ -223,6 +225,22 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
[self validateExecEvent:wantAction messageSetup:nil];
|
||||
}
|
||||
|
||||
- (void)stubRule:(SNTRule *)rule forIdentifiers:(struct RuleIdentifiers)wantIdentifiers {
|
||||
OCMStub([self.mockRuleDatabase ruleForIdentifiers:wantIdentifiers])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(^(NSInvocation *inv) {
|
||||
struct RuleIdentifiers gotIdentifiers = {};
|
||||
[inv getArgument:&gotIdentifiers atIndex:2];
|
||||
|
||||
XCTAssertEqualObjects(gotIdentifiers.cdhash, wantIdentifiers.cdhash);
|
||||
XCTAssertEqualObjects(gotIdentifiers.binarySHA256, wantIdentifiers.binarySHA256);
|
||||
XCTAssertEqualObjects(gotIdentifiers.signingID, wantIdentifiers.signingID);
|
||||
XCTAssertEqualObjects(gotIdentifiers.certificateSHA256, wantIdentifiers.certificateSHA256);
|
||||
XCTAssertEqualObjects(gotIdentifiers.teamID, wantIdentifiers.teamID);
|
||||
})
|
||||
.andReturn(rule);
|
||||
}
|
||||
|
||||
- (void)testBinaryAllowRule {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
|
||||
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
@@ -230,11 +248,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowBinary expected:@1];
|
||||
@@ -247,16 +262,96 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny];
|
||||
[self checkMetricCounters:kBlockBinary expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCDHashAllowRule {
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeCDHash;
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->cdhash[0] = 0xaa;
|
||||
msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD;
|
||||
}];
|
||||
[self checkMetricCounters:kAllowCDHash expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCDHashNoHardenedRuntimeRule {
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeCDHash;
|
||||
|
||||
// No CDHash should be set when hardened runtime CS flags are not set
|
||||
[self stubRule:rule forIdentifiers:{.cdhash = nil}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->cdhash[0] = 0xaa;
|
||||
// Ensure CS_HARD and CS_KILL are not set
|
||||
msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID;
|
||||
}];
|
||||
[self checkMetricCounters:kAllowCDHash expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCDHashBlockRule {
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeCDHash;
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->cdhash[0] = 0xaa;
|
||||
msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD;
|
||||
}];
|
||||
[self checkMetricCounters:kBlockCDHash expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCDHashAllowCompilerRule {
|
||||
OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(YES);
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeCDHash;
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllowCompiler
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->cdhash[0] = 0xaa;
|
||||
msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD;
|
||||
}];
|
||||
|
||||
[self checkMetricCounters:kAllowCompiler expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCDHashAllowCompilerRuleTransitiveRuleDisabled {
|
||||
OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(NO);
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeCDHash;
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->cdhash[0] = 0xaa;
|
||||
msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD;
|
||||
}];
|
||||
|
||||
[self checkMetricCounters:kAllowCDHash expected:@1];
|
||||
}
|
||||
|
||||
- (void)testSigningIDAllowRule {
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
@@ -264,17 +359,14 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
|
||||
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:signingID
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
[self stubRule:rule forIdentifiers:{ .signingID = signingID, .teamID = @(kExampleTeamID) }];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kExampleSigningID);
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kExampleTeamID);
|
||||
}];
|
||||
|
||||
[self checkMetricCounters:kAllowSigningID expected:@1];
|
||||
}
|
||||
|
||||
@@ -284,12 +376,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
rule.type = SNTRuleTypeSigningID;
|
||||
|
||||
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:signingID
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
[self stubRule:rule forIdentifiers:{ .signingID = signingID, .teamID = @(kExampleTeamID) }];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
@@ -300,19 +387,13 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
}
|
||||
|
||||
- (void)testTeamIDAllowRule {
|
||||
OCMStub([self.mockCodesignChecker signingInformation]).andReturn((@{
|
||||
(__bridge NSString *)kSecCodeInfoTeamIdentifier : @(kExampleTeamID),
|
||||
}));
|
||||
OCMStub([self.mockCodesignChecker teamID]).andReturn(@(kExampleTeamID));
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeTeamID;
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
[self stubRule:rule forIdentifiers:{ .teamID = @(kExampleTeamID) }];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
@@ -322,19 +403,13 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
}
|
||||
|
||||
- (void)testTeamIDBlockRule {
|
||||
OCMStub([self.mockCodesignChecker signingInformation]).andReturn((@{
|
||||
(__bridge NSString *)kSecCodeInfoTeamIdentifier : @(kExampleTeamID),
|
||||
}));
|
||||
OCMStub([self.mockCodesignChecker teamID]).andReturn(@(kExampleTeamID));
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeTeamID;
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
[self stubRule:rule forIdentifiers:{ .teamID = @(kExampleTeamID) }];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
@@ -353,11 +428,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeCertificate;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"a"
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.certificateSHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowCertificate expected:@1];
|
||||
@@ -373,11 +445,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeCertificate;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"a"
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.certificateSHA256 = @"a"}];
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
|
||||
@@ -395,11 +464,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllowCompiler];
|
||||
[self checkMetricCounters:kAllowCompiler expected:@1];
|
||||
@@ -413,11 +479,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowBinary expected:@1];
|
||||
@@ -431,11 +494,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowTransitive expected:@1];
|
||||
@@ -450,11 +510,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
|
||||
@@ -477,11 +534,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeSigningID;
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:signingID
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
[self stubRule:rule
|
||||
forIdentifiers:{ .binarySHA256 = @"a", .signingID = signingID, .teamID = @(kExampleTeamID) }];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllowCompiler
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
@@ -502,20 +556,11 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
rule.type = SNTRuleTypeSigningID;
|
||||
|
||||
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:signingID
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->signing_id = MakeESStringToken("com.google.santa.test");
|
||||
}];
|
||||
[self validateExecEvent:SNTActionRespondDeny];
|
||||
|
||||
OCMVerifyAllWithDelay(self.mockEventDatabase, 1);
|
||||
[self checkMetricCounters:kAllowSigningID expected:@0];
|
||||
@@ -557,7 +602,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
[self checkMetricCounters:kAllowUnknown expected:@1];
|
||||
}
|
||||
|
||||
- (void)testUnreadableFailOpenLockdown {
|
||||
- (void)testUnreadableFailOpen {
|
||||
// Undo the default mocks
|
||||
[self.mockFileInfo stopMocking];
|
||||
self.mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
@@ -565,15 +610,13 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
OCMStub([self.mockFileInfo alloc]).andReturn(nil);
|
||||
OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY error:[OCMArg setTo:nil]]).andReturn(nil);
|
||||
|
||||
// Lockdown mode, no fail-closed
|
||||
OCMStub([self.mockConfigurator failClosed]).andReturn(NO);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowNoFileInfo expected:@1];
|
||||
}
|
||||
|
||||
- (void)testUnreadableFailClosedLockdown {
|
||||
- (void)testUnreadableFailClosed {
|
||||
// Undo the default mocks
|
||||
[self.mockFileInfo stopMocking];
|
||||
self.mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
@@ -581,30 +624,12 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
OCMStub([self.mockFileInfo alloc]).andReturn(nil);
|
||||
OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY error:[OCMArg setTo:nil]]).andReturn(nil);
|
||||
|
||||
// Lockdown mode, fail-closed
|
||||
OCMStub([self.mockConfigurator failClosed]).andReturn(YES);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny];
|
||||
[self checkMetricCounters:kDenyNoFileInfo expected:@1];
|
||||
}
|
||||
|
||||
- (void)testUnreadableFailClosedMonitor {
|
||||
// Undo the default mocks
|
||||
[self.mockFileInfo stopMocking];
|
||||
self.mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
|
||||
OCMStub([self.mockFileInfo alloc]).andReturn(nil);
|
||||
OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY error:[OCMArg setTo:nil]]).andReturn(nil);
|
||||
|
||||
// Monitor mode, fail-closed
|
||||
OCMStub([self.mockConfigurator failClosed]).andReturn(YES);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeMonitor);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowNoFileInfo expected:@1];
|
||||
}
|
||||
|
||||
- (void)testMissingShasum {
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowScope expected:@1];
|
||||
@@ -638,11 +663,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
OCMVerifyAllWithDelay(self.mockEventDatabase, 1);
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
|
||||
@class MOLCodesignChecker;
|
||||
@class SNTCachedDecision;
|
||||
@@ -49,6 +51,13 @@
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback;
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc
|
||||
preCodesignCheckCallback:(void (^_Nullable)(void))preCodesignCheckCallback
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback;
|
||||
|
||||
///
|
||||
/// A wrapper for decisionForFileInfo:fileSHA256:certificateSHA256:. This method is slower as it
|
||||
@@ -57,9 +66,14 @@
|
||||
/// calculated, use the fileSHA256 parameter to save a second calculation of the hash.
|
||||
///
|
||||
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID;
|
||||
identifiers:(nonnull SNTRuleIdentifiers *)identifiers;
|
||||
|
||||
///
|
||||
/// Updates a decision for a given file and agent configuration.
|
||||
///
|
||||
/// Returns YES if the decision requires no futher processing NO otherwise.
|
||||
- (BOOL)decision:(nonnull SNTCachedDecision *)cd
|
||||
forRule:(nonnull SNTRule *)rule
|
||||
withTransitiveRules:(BOOL)transitive;
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
/// Copyright 2015 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 "Source/santad/SNTPolicyProcessor.h"
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#import <Security/SecCode.h>
|
||||
#import <Security/Security.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeepCopy.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
|
||||
@interface SNTPolicyProcessor ()
|
||||
@property SNTRuleTable *ruleTable;
|
||||
@property SNTConfigurator *configurator;
|
||||
@end
|
||||
|
||||
@implementation SNTPolicyProcessor
|
||||
|
||||
- (instancetype)initWithRuleTable:(SNTRuleTable *)ruleTable {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_ruleTable = ruleTable;
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID
|
||||
isProdSignedCallback:(BOOL (^_Nonnull)())isProdSignedCallback
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nullable)(
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.sha256 = fileSHA256 ?: fileInfo.SHA256;
|
||||
cd.teamID = teamID;
|
||||
cd.signingID = signingID;
|
||||
|
||||
SNTClientMode mode = [self.configurator clientMode];
|
||||
cd.decisionClientMode = mode;
|
||||
|
||||
// If the binary is a critical system binary, don't check its signature.
|
||||
// The binary was validated at startup when the rule table was initialized.
|
||||
SNTCachedDecision *systemCd = self.ruleTable.criticalSystemBinaries[cd.sha256];
|
||||
if (systemCd) {
|
||||
systemCd.decisionClientMode = mode;
|
||||
return systemCd;
|
||||
}
|
||||
|
||||
NSError *csInfoError;
|
||||
if (certificateSHA256.length) {
|
||||
cd.certSHA256 = certificateSHA256;
|
||||
} else {
|
||||
// Grab the code signature, if there's an error don't try to capture
|
||||
// any of the signature details.
|
||||
MOLCodesignChecker *csInfo = [fileInfo codesignCheckerWithError:&csInfoError];
|
||||
if (csInfoError) {
|
||||
csInfo = nil;
|
||||
cd.decisionExtra =
|
||||
[NSString stringWithFormat:@"Signature ignored due to error: %ld", (long)csInfoError.code];
|
||||
cd.teamID = nil;
|
||||
cd.signingID = nil;
|
||||
} else {
|
||||
cd.certSHA256 = csInfo.leafCertificate.SHA256;
|
||||
cd.certCommonName = csInfo.leafCertificate.commonName;
|
||||
cd.certChain = csInfo.certificates;
|
||||
cd.teamID = teamID
|
||||
?: [csInfo.signingInformation
|
||||
objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
|
||||
|
||||
// Ensure that if no teamID exists that the signing info confirms it is a
|
||||
// platform binary. If not, remove the signingID.
|
||||
if (!cd.teamID && cd.signingID) {
|
||||
id platformID = [csInfo.signingInformation
|
||||
objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
|
||||
if (![platformID isKindOfClass:[NSNumber class]] || [platformID intValue] == 0) {
|
||||
cd.signingID = nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *entitlements =
|
||||
csInfo.signingInformation[(__bridge NSString *)kSecCodeInfoEntitlementsDict];
|
||||
|
||||
if (entitlementsFilterCallback) {
|
||||
cd.entitlements = entitlementsFilterCallback(entitlements);
|
||||
cd.entitlementsFiltered = (cd.entitlements.count == entitlements.count);
|
||||
} else {
|
||||
cd.entitlements = [entitlements sntDeepCopy];
|
||||
cd.entitlementsFiltered = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
cd.quarantineURL = fileInfo.quarantineDataURL;
|
||||
|
||||
// Do not evaluate TeamID/SigningID rules for dev-signed code based on the
|
||||
// assumption that orgs are generally more relaxed about dev signed cert
|
||||
// protections and users can more easily produce dev-signed code that
|
||||
// would otherwise be inadvertently allowed.
|
||||
// Note: Only perform the check if the SigningID is still set, otherwise
|
||||
// it is unsigned or had issues above that already cleared the values.
|
||||
if (cd.signingID && !isProdSignedCallback()) {
|
||||
LOGD(@"Ignoring TeamID and SigningID rules for code not signed with production cert: %@",
|
||||
cd.signingID);
|
||||
cd.teamID = nil;
|
||||
cd.signingID = nil;
|
||||
}
|
||||
|
||||
SNTRule *rule = [self.ruleTable ruleForBinarySHA256:cd.sha256
|
||||
signingID:cd.signingID
|
||||
certificateSHA256:cd.certSHA256
|
||||
teamID:cd.teamID];
|
||||
if (rule) {
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeBinary:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowBinary; return cd;
|
||||
case SNTRuleStateSilentBlock: cd.silentBlock = YES;
|
||||
case SNTRuleStateBlock:
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
cd.decision = SNTEventStateBlockBinary;
|
||||
return cd;
|
||||
case SNTRuleStateAllowCompiler:
|
||||
// If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules
|
||||
// become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if
|
||||
// it were SNTRuleStateAllow.
|
||||
if ([self.configurator enableTransitiveRules]) {
|
||||
cd.decision = SNTEventStateAllowCompiler;
|
||||
} else {
|
||||
cd.decision = SNTEventStateAllowBinary;
|
||||
}
|
||||
return cd;
|
||||
case SNTRuleStateAllowTransitive:
|
||||
// If transitive rules are enabled, then SNTRuleStateAllowTransitive
|
||||
// rules become SNTEventStateAllowTransitive decisions. Otherwise, we treat the
|
||||
// rule as if it were SNTRuleStateUnknown.
|
||||
if ([self.configurator enableTransitiveRules]) {
|
||||
cd.decision = SNTEventStateAllowTransitive;
|
||||
return cd;
|
||||
} else {
|
||||
rule.state = SNTRuleStateUnknown;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case SNTRuleTypeSigningID:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowSigningID; return cd;
|
||||
case SNTRuleStateAllowCompiler:
|
||||
// If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules
|
||||
// become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if
|
||||
// it were SNTRuleStateAllowSigningID.
|
||||
if ([self.configurator enableTransitiveRules]) {
|
||||
cd.decision = SNTEventStateAllowCompiler;
|
||||
} else {
|
||||
cd.decision = SNTEventStateAllowSigningID;
|
||||
}
|
||||
return cd;
|
||||
case SNTRuleStateSilentBlock:
|
||||
cd.silentBlock = YES;
|
||||
// intentional fallthrough
|
||||
case SNTRuleStateBlock:
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
cd.decision = SNTEventStateBlockSigningID;
|
||||
return cd;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case SNTRuleTypeCertificate:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCertificate; return cd;
|
||||
case SNTRuleStateSilentBlock:
|
||||
cd.silentBlock = YES;
|
||||
// intentional fallthrough
|
||||
case SNTRuleStateBlock:
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
cd.decision = SNTEventStateBlockCertificate;
|
||||
return cd;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case SNTRuleTypeTeamID:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowTeamID; return cd;
|
||||
case SNTRuleStateSilentBlock:
|
||||
cd.silentBlock = YES;
|
||||
// intentional fallthrough
|
||||
case SNTRuleStateBlock:
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
cd.decision = SNTEventStateBlockTeamID;
|
||||
return cd;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
if ([[SNTConfigurator configurator] enableBadSignatureProtection] && csInfoError &&
|
||||
csInfoError.code != errSecCSUnsigned) {
|
||||
cd.decisionExtra =
|
||||
[NSString stringWithFormat:@"Blocked due to signature error: %ld", (long)csInfoError.code];
|
||||
cd.decision = SNTEventStateBlockCertificate;
|
||||
return cd;
|
||||
}
|
||||
|
||||
NSString *msg = [self fileIsScopeBlocked:fileInfo];
|
||||
if (msg) {
|
||||
cd.decisionExtra = msg;
|
||||
cd.decision = SNTEventStateBlockScope;
|
||||
return cd;
|
||||
}
|
||||
|
||||
msg = [self fileIsScopeAllowed:fileInfo];
|
||||
if (msg) {
|
||||
cd.decisionExtra = msg;
|
||||
cd.decision = SNTEventStateAllowScope;
|
||||
return cd;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case SNTClientModeMonitor: cd.decision = SNTEventStateAllowUnknown; return cd;
|
||||
case SNTClientModeLockdown: cd.decision = SNTEventStateBlockUnknown; return cd;
|
||||
default: cd.decision = SNTEventStateBlockUnknown; return cd;
|
||||
}
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
NSString *signingID;
|
||||
NSString *teamID;
|
||||
|
||||
const char *entitlementsFilterTeamID = NULL;
|
||||
|
||||
if (targetProc->signing_id.length > 0) {
|
||||
if (targetProc->team_id.length > 0) {
|
||||
entitlementsFilterTeamID = targetProc->team_id.data;
|
||||
teamID = [NSString stringWithUTF8String:targetProc->team_id.data];
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"%@:%@", teamID,
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
} else if (targetProc->is_platform_binary) {
|
||||
entitlementsFilterTeamID = "platform";
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"platform:%@",
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
}
|
||||
}
|
||||
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
fileSHA256:nil
|
||||
certificateSHA256:nil
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
isProdSignedCallback:^BOOL {
|
||||
return ((targetProc->codesigning_flags & CS_DEV_CODE) == 0);
|
||||
}
|
||||
entitlementsFilterCallback:^NSDictionary *(NSDictionary *entitlements) {
|
||||
return entitlementsFilterCallback(entitlementsFilterTeamID, entitlements);
|
||||
}];
|
||||
}
|
||||
|
||||
// Used by `$ santactl fileinfo`.
|
||||
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID {
|
||||
MOLCodesignChecker *csInfo;
|
||||
NSError *error;
|
||||
|
||||
SNTFileInfo *fileInfo = [[SNTFileInfo alloc] initWithPath:filePath error:&error];
|
||||
if (!fileInfo) {
|
||||
LOGW(@"Failed to read file %@: %@", filePath, error.localizedDescription);
|
||||
} else {
|
||||
csInfo = [fileInfo codesignCheckerWithError:&error];
|
||||
if (error) {
|
||||
LOGW(@"Failed to get codesign ingo for file %@: %@", filePath, error.localizedDescription);
|
||||
}
|
||||
}
|
||||
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
isProdSignedCallback:^BOOL {
|
||||
if (csInfo) {
|
||||
// Development OID values defined by Apple and used by the Security Framework
|
||||
// https://images.apple.com/certificateauthority/pdf/Apple_WWDR_CPS_v1.31.pdf
|
||||
NSArray *keys = @[ @"1.2.840.113635.100.6.1.2", @"1.2.840.113635.100.6.1.12" ];
|
||||
NSDictionary *vals = CFBridgingRelease(SecCertificateCopyValues(
|
||||
csInfo.leafCertificate.certRef, (__bridge CFArrayRef)keys, NULL));
|
||||
return vals.count == 0;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
entitlementsFilterCallback:nil];
|
||||
}
|
||||
|
||||
///
|
||||
/// Checks whether the file at @c path is in-scope for checking with Santa.
|
||||
///
|
||||
/// Files that are out of scope:
|
||||
/// + Non Mach-O files that are not part of an installer package.
|
||||
/// + Files in allowed path.
|
||||
///
|
||||
/// @return @c YES if file is in scope, @c NO otherwise.
|
||||
///
|
||||
- (NSString *)fileIsScopeAllowed:(SNTFileInfo *)fi {
|
||||
if (!fi) return nil;
|
||||
|
||||
// Determine if file is within an allowed path
|
||||
NSRegularExpression *re = [[SNTConfigurator configurator] allowedPathRegex];
|
||||
if ([re numberOfMatchesInString:fi.path options:0 range:NSMakeRange(0, fi.path.length)]) {
|
||||
return @"Allowed Path Regex";
|
||||
}
|
||||
|
||||
// If file is not a Mach-O file, we're not interested.
|
||||
if (!fi.isMachO) {
|
||||
return @"Not a Mach-O";
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)fileIsScopeBlocked:(SNTFileInfo *)fi {
|
||||
if (!fi) return nil;
|
||||
|
||||
NSRegularExpression *re = [[SNTConfigurator configurator] blockedPathRegex];
|
||||
if ([re numberOfMatchesInString:fi.path options:0 range:NSMakeRange(0, fi.path.length)]) {
|
||||
return @"Blocked Path Regex";
|
||||
}
|
||||
|
||||
if ([[SNTConfigurator configurator] enablePageZeroProtection] && fi.isMissingPageZero) {
|
||||
return @"Missing __PAGEZERO";
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
423
Source/santad/SNTPolicyProcessor.mm
Normal file
423
Source/santad/SNTPolicyProcessor.mm
Normal file
@@ -0,0 +1,423 @@
|
||||
/// Copyright 2015 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 "Source/santad/SNTPolicyProcessor.h"
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#import <Security/SecCode.h>
|
||||
#import <Security/Security.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeepCopy.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
|
||||
@interface SNTPolicyProcessor ()
|
||||
@property SNTRuleTable *ruleTable;
|
||||
@property SNTConfigurator *configurator;
|
||||
@end
|
||||
|
||||
@implementation SNTPolicyProcessor
|
||||
|
||||
- (instancetype)initWithRuleTable:(SNTRuleTable *)ruleTable {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_ruleTable = ruleTable;
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// This method applies the rules to the cached decision object.
|
||||
//
|
||||
// It returns YES if the decision was made, NO if the decision was not made.
|
||||
- (BOOL)decision:(SNTCachedDecision *)cd
|
||||
forRule:(SNTRule *)rule
|
||||
withTransitiveRules:(BOOL)enableTransitiveRules {
|
||||
static const auto decisions =
|
||||
absl::flat_hash_map<std::pair<SNTRuleType, SNTRuleState>, SNTEventState>{
|
||||
{{SNTRuleTypeCDHash, SNTRuleStateAllow}, SNTEventStateAllowCDHash},
|
||||
{{SNTRuleTypeCDHash, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler},
|
||||
{{SNTRuleTypeCDHash, SNTRuleStateBlock}, SNTEventStateBlockCDHash},
|
||||
{{SNTRuleTypeCDHash, SNTRuleStateSilentBlock}, SNTEventStateBlockCDHash},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateAllow}, SNTEventStateAllowBinary},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateAllowTransitive}, SNTEventStateAllowTransitive},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateSilentBlock}, SNTEventStateBlockBinary},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateBlock}, SNTEventStateBlockBinary},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateAllow}, SNTEventStateAllowSigningID},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateSilentBlock}, SNTEventStateBlockSigningID},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateBlock}, SNTEventStateBlockSigningID},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateAllow}, SNTEventStateAllowCertificate},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateSilentBlock}, SNTEventStateBlockCertificate},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateBlock}, SNTEventStateBlockCertificate},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateAllow}, SNTEventStateAllowTeamID},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateSilentBlock}, SNTEventStateBlockTeamID},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateBlock}, SNTEventStateBlockTeamID},
|
||||
};
|
||||
|
||||
auto iterator = decisions.find(std::pair<SNTRuleType, SNTRuleState>{rule.type, rule.state});
|
||||
if (iterator != decisions.end()) {
|
||||
cd.decision = iterator->second;
|
||||
} else {
|
||||
// If we have an invalid state combination then either we have stale data in
|
||||
// the database or a programming error. We treat this as if the
|
||||
// corresponding rule was not found.
|
||||
LOGE(@"Invalid rule type/state combination %ld/%ld", rule.type, rule.state);
|
||||
return NO;
|
||||
}
|
||||
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateSilentBlock: cd.silentBlock = YES; break;
|
||||
case SNTRuleStateAllowCompiler:
|
||||
if (!enableTransitiveRules) {
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeCDHash: cd.decision = SNTEventStateAllowCDHash; break;
|
||||
case SNTRuleTypeBinary: cd.decision = SNTEventStateAllowBinary; break;
|
||||
case SNTRuleTypeSigningID: cd.decision = SNTEventStateAllowSigningID; break;
|
||||
default:
|
||||
// Programming error. Something's marked as a compiler that shouldn't
|
||||
// be.
|
||||
LOGE(@"Invalid compiler rule type %ld", rule.type);
|
||||
[NSException
|
||||
raise:@"Invalid compiler rule type"
|
||||
format:@"decision:forRule:withTransitiveRules: Unexpected compiler rule type: %ld",
|
||||
rule.type];
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SNTRuleStateAllowTransitive:
|
||||
// If transitive rules are disabled, then we treat
|
||||
// SNTRuleStateAllowTransitive rules as if a matching rule was not found
|
||||
// and set the state to unknown. Otherwise the decision map will have already set
|
||||
// the EventState to SNTEventStateAllowTransitive.
|
||||
if (!enableTransitiveRules) {
|
||||
cd.decision = SNTEventStateUnknown;
|
||||
return NO;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// If its not one of the special cases above, we don't need to do anything.
|
||||
break;
|
||||
}
|
||||
|
||||
// We know we have a match so apply the custom messages
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
static void UpdateCachedDecisionSigningInfo(
|
||||
SNTCachedDecision *cd, MOLCodesignChecker *csInfo,
|
||||
NSDictionary *_Nullable (^entitlementsFilterCallback)(NSDictionary *_Nullable entitlements)) {
|
||||
cd.certSHA256 = csInfo.leafCertificate.SHA256;
|
||||
cd.certCommonName = csInfo.leafCertificate.commonName;
|
||||
cd.certChain = csInfo.certificates;
|
||||
// Check if we need to get teamID from code signing.
|
||||
if (!cd.teamID) {
|
||||
cd.teamID = csInfo.teamID;
|
||||
}
|
||||
|
||||
// Ensure that if no teamID exists that the signing info confirms it is a
|
||||
// platform binary. If not, remove the signingID.
|
||||
if (!cd.teamID && cd.signingID) {
|
||||
if (!csInfo.platformBinary) {
|
||||
cd.signingID = nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *entitlements = csInfo.entitlements;
|
||||
|
||||
if (entitlementsFilterCallback) {
|
||||
cd.entitlements = entitlementsFilterCallback(entitlements);
|
||||
cd.entitlementsFiltered = (cd.entitlements.count != entitlements.count);
|
||||
} else {
|
||||
cd.entitlements = [entitlements sntDeepCopy];
|
||||
cd.entitlementsFiltered = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)
|
||||
decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
cdhash:(nullable NSString *)cdhash
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID
|
||||
isProdSignedCallback:(BOOL (^_Nonnull)())isProdSignedCallback
|
||||
entitlementsFilterCallback:(NSDictionary *_Nullable (^_Nullable)(
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback
|
||||
preCodesignCheckCallback:(void (^_Nullable)(void))preCodesignCheckCallback {
|
||||
// Check the hash before allocating a SNTCachedDecision.
|
||||
NSString *fileHash = fileSHA256 ?: fileInfo.SHA256;
|
||||
SNTClientMode mode = [self.configurator clientMode];
|
||||
|
||||
// If the binary is a critical system binary, don't check its signature.
|
||||
// The binary was validated at startup when the rule table was initialized.
|
||||
SNTCachedDecision *systemCd = self.ruleTable.criticalSystemBinaries[fileHash];
|
||||
if (systemCd) {
|
||||
systemCd.decisionClientMode = mode;
|
||||
return systemCd;
|
||||
}
|
||||
|
||||
// Allocate a new cached decision for the execution.
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.cdhash = cdhash;
|
||||
cd.sha256 = fileHash;
|
||||
cd.teamID = teamID;
|
||||
cd.signingID = signingID;
|
||||
cd.decisionClientMode = mode;
|
||||
cd.quarantineURL = fileInfo.quarantineDataURL;
|
||||
|
||||
NSError *csInfoError;
|
||||
if (certificateSHA256.length) {
|
||||
cd.certSHA256 = certificateSHA256;
|
||||
} else {
|
||||
if (preCodesignCheckCallback) {
|
||||
preCodesignCheckCallback();
|
||||
}
|
||||
|
||||
// Grab the code signature, if there's an error don't try to capture
|
||||
// any of the signature details. Also clear out any rule lookup parameters
|
||||
// that would require being validly signed.
|
||||
MOLCodesignChecker *csInfo = [fileInfo codesignCheckerWithError:&csInfoError];
|
||||
if (csInfoError) {
|
||||
csInfo = nil;
|
||||
cd.decisionExtra =
|
||||
[NSString stringWithFormat:@"Signature ignored due to error: %ld", (long)csInfoError.code];
|
||||
cd.teamID = nil;
|
||||
cd.signingID = nil;
|
||||
cd.cdhash = nil;
|
||||
} else {
|
||||
UpdateCachedDecisionSigningInfo(cd, csInfo, entitlementsFilterCallback);
|
||||
}
|
||||
}
|
||||
|
||||
// Do not evaluate TeamID/SigningID rules for dev-signed code based on the
|
||||
// assumption that orgs are generally more relaxed about dev signed cert
|
||||
// protections and users can more easily produce dev-signed code that
|
||||
// would otherwise be inadvertently allowed.
|
||||
// Note: Only perform the check if the SigningID is still set, otherwise
|
||||
// it is unsigned or had issues above that already cleared the values.
|
||||
if (cd.signingID && !isProdSignedCallback()) {
|
||||
LOGD(@"Ignoring TeamID and SigningID rules for code not signed with production cert: %@",
|
||||
cd.signingID);
|
||||
cd.teamID = nil;
|
||||
cd.signingID = nil;
|
||||
}
|
||||
|
||||
SNTRule *rule =
|
||||
[self.ruleTable ruleForIdentifiers:(struct RuleIdentifiers){.cdhash = cd.cdhash,
|
||||
.binarySHA256 = cd.sha256,
|
||||
.signingID = cd.signingID,
|
||||
.certificateSHA256 = cd.certSHA256,
|
||||
.teamID = cd.teamID}];
|
||||
if (rule) {
|
||||
// If we have a rule match we don't need to process any further.
|
||||
if ([self decision:cd
|
||||
forRule:rule
|
||||
withTransitiveRules:self.configurator.enableTransitiveRules]) {
|
||||
return cd;
|
||||
}
|
||||
}
|
||||
|
||||
if ([[SNTConfigurator configurator] enableBadSignatureProtection] && csInfoError &&
|
||||
csInfoError.code != errSecCSUnsigned) {
|
||||
cd.decisionExtra =
|
||||
[NSString stringWithFormat:@"Blocked due to signature error: %ld", (long)csInfoError.code];
|
||||
cd.decision = SNTEventStateBlockCertificate;
|
||||
return cd;
|
||||
}
|
||||
|
||||
NSString *msg = [self fileIsScopeBlocked:fileInfo];
|
||||
if (msg) {
|
||||
cd.decisionExtra = msg;
|
||||
cd.decision = SNTEventStateBlockScope;
|
||||
return cd;
|
||||
}
|
||||
|
||||
msg = [self fileIsScopeAllowed:fileInfo];
|
||||
if (msg) {
|
||||
cd.decisionExtra = msg;
|
||||
cd.decision = SNTEventStateAllowScope;
|
||||
return cd;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case SNTClientModeMonitor: cd.decision = SNTEventStateAllowUnknown; return cd;
|
||||
case SNTClientModeLockdown: cd.decision = SNTEventStateBlockUnknown; return cd;
|
||||
default: cd.decision = SNTEventStateBlockUnknown; return cd;
|
||||
}
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
targetProcess:targetProc
|
||||
preCodesignCheckCallback:nil
|
||||
entitlementsFilterCallback:entitlementsFilterCallback];
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc
|
||||
preCodesignCheckCallback:(void (^_Nullable)(void))preCodesignCheckCallback
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
NSString *signingID;
|
||||
NSString *teamID;
|
||||
NSString *cdhash;
|
||||
|
||||
const char *entitlementsFilterTeamID = NULL;
|
||||
|
||||
if (targetProc->codesigning_flags & CS_SIGNED && targetProc->codesigning_flags & CS_VALID) {
|
||||
if (targetProc->signing_id.length > 0) {
|
||||
if (targetProc->team_id.length > 0) {
|
||||
entitlementsFilterTeamID = targetProc->team_id.data;
|
||||
teamID = [NSString stringWithUTF8String:targetProc->team_id.data];
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"%@:%@", teamID,
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
} else if (targetProc->is_platform_binary) {
|
||||
entitlementsFilterTeamID = "platform";
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"platform:%@",
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
}
|
||||
}
|
||||
|
||||
// Only consider the CDHash for processes that have CS_KILL or CS_HARD set.
|
||||
// This ensures that the OS will kill the process if the CDHash was tampered
|
||||
// with and code was loaded that didn't match a page hash.
|
||||
if (targetProc->codesigning_flags & CS_KILL || targetProc->codesigning_flags & CS_HARD) {
|
||||
static NSString *const kCDHashFormatString = @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
|
||||
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x";
|
||||
|
||||
const uint8_t *buf = targetProc->cdhash;
|
||||
cdhash = [[NSString alloc] initWithFormat:kCDHashFormatString, buf[0], buf[1], buf[2], buf[3],
|
||||
buf[4], buf[5], buf[6], buf[7], buf[8], buf[9],
|
||||
buf[10], buf[11], buf[12], buf[13], buf[14],
|
||||
buf[15], buf[16], buf[17], buf[18], buf[19]];
|
||||
}
|
||||
}
|
||||
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
cdhash:cdhash
|
||||
fileSHA256:nil
|
||||
certificateSHA256:nil
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
isProdSignedCallback:^BOOL {
|
||||
return ((targetProc->codesigning_flags & CS_DEV_CODE) == 0);
|
||||
}
|
||||
entitlementsFilterCallback:^NSDictionary *(NSDictionary *entitlements) {
|
||||
return entitlementsFilterCallback(entitlementsFilterTeamID, entitlements);
|
||||
}
|
||||
preCodesignCheckCallback:preCodesignCheckCallback];
|
||||
}
|
||||
|
||||
// Used by `$ santactl fileinfo`.
|
||||
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
|
||||
identifiers:(nonnull SNTRuleIdentifiers *)identifiers {
|
||||
MOLCodesignChecker *csInfo;
|
||||
NSError *error;
|
||||
|
||||
SNTFileInfo *fileInfo = [[SNTFileInfo alloc] initWithPath:filePath error:&error];
|
||||
if (!fileInfo) {
|
||||
LOGW(@"Failed to read file %@: %@", filePath, error.localizedDescription);
|
||||
} else {
|
||||
csInfo = [fileInfo codesignCheckerWithError:&error];
|
||||
if (error) {
|
||||
LOGW(@"Failed to get codesign ingo for file %@: %@", filePath, error.localizedDescription);
|
||||
}
|
||||
}
|
||||
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
cdhash:identifiers.cdhash
|
||||
fileSHA256:identifiers.binarySHA256
|
||||
certificateSHA256:identifiers.certificateSHA256
|
||||
teamID:identifiers.teamID
|
||||
signingID:identifiers.signingID
|
||||
isProdSignedCallback:^BOOL {
|
||||
if (csInfo) {
|
||||
// Development OID values defined by Apple and used by the Security Framework
|
||||
// https://images.apple.com/certificateauthority/pdf/Apple_WWDR_CPS_v1.31.pdf
|
||||
NSArray *keys = @[ @"1.2.840.113635.100.6.1.2", @"1.2.840.113635.100.6.1.12" ];
|
||||
NSDictionary *vals = CFBridgingRelease(SecCertificateCopyValues(
|
||||
csInfo.leafCertificate.certRef, (__bridge CFArrayRef)keys, NULL));
|
||||
return vals.count == 0;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
entitlementsFilterCallback:nil
|
||||
preCodesignCheckCallback:nil];
|
||||
}
|
||||
|
||||
///
|
||||
/// Checks whether the file at @c path is in-scope for checking with Santa.
|
||||
///
|
||||
/// Files that are out of scope:
|
||||
/// + Non Mach-O files that are not part of an installer package.
|
||||
/// + Files in allowed path.
|
||||
///
|
||||
/// @return @c YES if file is in scope, @c NO otherwise.
|
||||
///
|
||||
- (NSString *)fileIsScopeAllowed:(SNTFileInfo *)fi {
|
||||
if (!fi) return nil;
|
||||
|
||||
// Determine if file is within an allowed path
|
||||
NSRegularExpression *re = [[SNTConfigurator configurator] allowedPathRegex];
|
||||
if ([re numberOfMatchesInString:fi.path options:0 range:NSMakeRange(0, fi.path.length)]) {
|
||||
return @"Allowed Path Regex";
|
||||
}
|
||||
|
||||
// If file is not a Mach-O file, we're not interested.
|
||||
if (!fi.isMachO) {
|
||||
return @"Not a Mach-O";
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)fileIsScopeBlocked:(SNTFileInfo *)fi {
|
||||
if (!fi) return nil;
|
||||
|
||||
NSRegularExpression *re = [[SNTConfigurator configurator] blockedPathRegex];
|
||||
if ([re numberOfMatchesInString:fi.path options:0 range:NSMakeRange(0, fi.path.length)]) {
|
||||
return @"Blocked Path Regex";
|
||||
}
|
||||
|
||||
if ([[SNTConfigurator configurator] enablePageZeroProtection] && fi.isMissingPageZero) {
|
||||
return @"Missing __PAGEZERO";
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
564
Source/santad/SNTPolicyProcessorTest.mm
Normal file
564
Source/santad/SNTPolicyProcessorTest.mm
Normal file
@@ -0,0 +1,564 @@
|
||||
/// Copyright 2024 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#import "Source/santad/SNTPolicyProcessor.h"
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
#import "Source/santad/SNTPolicyProcessor.h"
|
||||
|
||||
@interface SNTPolicyProcessorTest : XCTestCase
|
||||
@property SNTPolicyProcessor *processor;
|
||||
@end
|
||||
|
||||
@implementation SNTPolicyProcessorTest
|
||||
- (void)setUp {
|
||||
self.processor = [[SNTPolicyProcessor alloc] init];
|
||||
}
|
||||
|
||||
- (void)testRule:(SNTRule *)rule
|
||||
transitiveRules:(BOOL)transitiveRules
|
||||
final:(BOOL)final
|
||||
matches:(BOOL)matches
|
||||
silent:(BOOL)silent
|
||||
expectedDecision:(SNTEventState)decision {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
if (matches) {
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeBinary: cd.sha256 = rule.identifier; break;
|
||||
case SNTRuleTypeCertificate: cd.certSHA256 = rule.identifier; break;
|
||||
case SNTRuleTypeCDHash: cd.cdhash = rule.identifier; break;
|
||||
default: break;
|
||||
}
|
||||
} else {
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeBinary:
|
||||
cd.sha256 = @"2334567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
|
||||
break;
|
||||
case SNTRuleTypeCertificate:
|
||||
cd.certSHA256 = @"2234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
|
||||
break;
|
||||
case SNTRuleTypeCDHash: cd.cdhash = @"b023fbe5361a5bbd793dc3889556e93f41ec9bb8"; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
BOOL decisionIsFinal = [self.processor decision:cd
|
||||
forRule:rule
|
||||
withTransitiveRules:transitiveRules];
|
||||
XCTAssertEqual(cd.decision, decision);
|
||||
XCTAssertEqual(decisionIsFinal, final);
|
||||
XCTAssertEqual(cd.silentBlock, silent);
|
||||
}
|
||||
|
||||
- (void)testDecisionForBlockByCDHashRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"CDHASH",
|
||||
@"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8",
|
||||
@"policy" : @"BLOCKLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateBlockCDHash];
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateBlockCDHash];
|
||||
}
|
||||
|
||||
- (void)testDecisionForSilentBlockByCDHashRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"CDHASH",
|
||||
@"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8",
|
||||
@"policy" : @"SILENT_BLOCKLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:YES
|
||||
expectedDecision:SNTEventStateBlockCDHash];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:YES
|
||||
expectedDecision:SNTEventStateBlockCDHash];
|
||||
}
|
||||
|
||||
- (void)testDecisionForAllowbyCDHashRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"CDHASH",
|
||||
@"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8",
|
||||
@"policy" : @"ALLOWLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowCDHash];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowCDHash];
|
||||
}
|
||||
|
||||
- (void)testDecisionForBlockBySHA256RuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"BINARY",
|
||||
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
@"policy" : @"BLOCKLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateBlockBinary];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateBlockBinary];
|
||||
}
|
||||
|
||||
- (void)testDecisionForSilenBlockBySHA256RuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"BINARY",
|
||||
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
@"policy" : @"SILENT_BLOCKLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:YES
|
||||
expectedDecision:SNTEventStateBlockBinary];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:YES
|
||||
expectedDecision:SNTEventStateBlockBinary];
|
||||
}
|
||||
|
||||
- (void)testDecisionForAllowBySHA256RuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"BINARY",
|
||||
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
@"policy" : @"ALLOWLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowBinary];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowBinary];
|
||||
}
|
||||
|
||||
- (void)testDecisionForSigningIDBlockRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"SIGNINGID",
|
||||
@"identifier" : @"ABCDEFGHIJ:ABCDEFGHIJ",
|
||||
@"policy" : @"BLOCKLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateBlockSigningID];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateBlockSigningID];
|
||||
}
|
||||
|
||||
// Signing ID rules
|
||||
- (void)testDecisionForSigningIDSilentBlockRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"SIGNINGID",
|
||||
@"identifier" : @"TEAMID1234:ABCDEFGHIJ",
|
||||
@"policy" : @"SILENT_BLOCKLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:YES
|
||||
expectedDecision:SNTEventStateBlockSigningID];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:YES
|
||||
expectedDecision:SNTEventStateBlockSigningID];
|
||||
}
|
||||
|
||||
- (void)testDecisionForSigningIDAllowRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"SIGNINGID",
|
||||
@"identifier" : @"TEAMID1234:ABCDEFGHIJ",
|
||||
@"policy" : @"ALLOWLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowSigningID];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowSigningID];
|
||||
}
|
||||
|
||||
// Certificate rules
|
||||
- (void)testDecisionForCertificateBlockRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"CERTIFICATE",
|
||||
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
@"policy" : @"BLOCKLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateBlockCertificate];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateBlockCertificate];
|
||||
}
|
||||
|
||||
- (void)testDecisionForCertificateSilentBlockRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"CERTIFICATE",
|
||||
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
@"policy" : @"SILENT_BLOCKLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:YES
|
||||
expectedDecision:SNTEventStateBlockCertificate];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:YES
|
||||
expectedDecision:SNTEventStateBlockCertificate];
|
||||
}
|
||||
|
||||
- (void)testDecisionForCertificateAllowRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"CERTIFICATE",
|
||||
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
@"policy" : @"ALLOWLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowCertificate];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowCertificate];
|
||||
}
|
||||
|
||||
// Team ID rules
|
||||
- (void)testDecisionForTeamIDBlockRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"TEAMID",
|
||||
@"identifier" : @"TEAMID1234",
|
||||
@"policy" : @"BLOCKLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateBlockTeamID];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateBlockTeamID];
|
||||
}
|
||||
|
||||
- (void)testDecisionForTeamIDSilentBlockRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"TEAMID",
|
||||
@"identifier" : @"TEAMID1234",
|
||||
@"policy" : @"SILENT_BLOCKLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:YES
|
||||
expectedDecision:SNTEventStateBlockTeamID];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:YES
|
||||
expectedDecision:SNTEventStateBlockTeamID];
|
||||
}
|
||||
|
||||
- (void)testDecisionForTeamIDAllowRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"TEAMID",
|
||||
@"identifier" : @"TEAMID1234",
|
||||
@"policy" : @"ALLOWLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowTeamID];
|
||||
// Ensure that nothing changes when disabling transitive rules.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowTeamID];
|
||||
}
|
||||
|
||||
// Compiler rules
|
||||
// CDHash
|
||||
- (void)testDecisionForCDHashCompilerRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"CDHASH",
|
||||
@"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8",
|
||||
@"policy" : @"ALLOWLIST_COMPILER"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowCompiler];
|
||||
// Ensure disabling transitive rules results in a binary allow
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowCDHash];
|
||||
}
|
||||
|
||||
// SHA256
|
||||
- (void)testDecisionForSHA256CompilerRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"BINARY",
|
||||
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
@"policy" : @"ALLOWLIST_COMPILER"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowCompiler];
|
||||
// Ensure disabling transitive rules results in a binary allow
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowBinary];
|
||||
}
|
||||
|
||||
// SigningID
|
||||
- (void)testDecisionForSigningIDCompilerRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"SIGNINGID",
|
||||
@"identifier" : @"TEAMID1234:ABCDEFGHIJ",
|
||||
@"policy" : @"ALLOWLIST_COMPILER"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowCompiler];
|
||||
// Ensure disabling transitive rules results in a Signing ID allow
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowSigningID];
|
||||
}
|
||||
|
||||
// Transitive allowlist rules
|
||||
- (void)testDecisionForTransitiveAllowlistRuleMatches {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"BINARY",
|
||||
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
@"policy" : @"ALLOWLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:YES
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateAllowTransitive];
|
||||
// Ensure that a transitive allowlist rule results in an
|
||||
// SNTEventStateUnknown if transitive rules are disabled.
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:NO
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateUnknown];
|
||||
}
|
||||
|
||||
- (void)testEnsureANonMatchingRuleResultsInUnknown {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"BINARY",
|
||||
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
@"policy" : @"ALLOWLIST"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
|
||||
rule.state = static_cast<SNTRuleState>(88888); // Set to an invalid state
|
||||
|
||||
[self testRule:rule
|
||||
transitiveRules:YES
|
||||
final:NO
|
||||
matches:NO
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateUnknown];
|
||||
|
||||
[self testRule:rule
|
||||
transitiveRules:NO
|
||||
final:NO
|
||||
matches:YES
|
||||
silent:NO
|
||||
expectedDecision:SNTEventStateUnknown];
|
||||
}
|
||||
|
||||
- (void)testEnsureCustomURLAndMessageAreSet {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"rule_type" : @"BINARY",
|
||||
@"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"custom_msg" : @"Custom Message",
|
||||
@"custom_url" : @"https://example.com"
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(rule, "invalid test rule dictionary");
|
||||
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.sha256 = rule.identifier;
|
||||
|
||||
[self.processor decision:cd forRule:rule withTransitiveRules:YES];
|
||||
|
||||
XCTAssertEqualObjects(cd.customMsg, @"Custom Message");
|
||||
XCTAssertEqualObjects(cd.customURL, @"https://example.com");
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
#import "Source/santad/SNTExecutionController.h"
|
||||
#import "Source/santad/SNTNotificationQueue.h"
|
||||
@@ -47,6 +48,7 @@ void SantadMain(
|
||||
SNTNotificationQueue* notifier_queue, SNTSyncdQueue* syncd_queue,
|
||||
SNTExecutionController* exec_controller,
|
||||
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree,
|
||||
std::shared_ptr<santa::santad::TTYWriter> tty_writer);
|
||||
std::shared_ptr<santa::santad::TTYWriter> tty_writer,
|
||||
std::shared_ptr<santa::santad::process_tree::ProcessTree> process_tree);
|
||||
|
||||
#endif
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user