mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
31 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 |
@@ -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
|
||||
|
||||
@@ -210,7 +210,7 @@ objc_library(
|
||||
],
|
||||
deps = [
|
||||
":CertificateHelpers",
|
||||
"@MOLCertificate",
|
||||
":SNTStoredEvent",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -478,13 +478,16 @@ santa_unit_test(
|
||||
name = "SNTBlockMessageTest",
|
||||
srcs = ["SNTBlockMessageTest.m"],
|
||||
deps = [
|
||||
":SNTBlockMessage",
|
||||
":SNTBlockMessage_SantaGUI",
|
||||
":SNTConfigurator",
|
||||
":SNTFileAccessEvent",
|
||||
":SNTStoredEvent",
|
||||
":SNTSystemInfo",
|
||||
"@OCMock",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"AppKit",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -182,6 +182,7 @@ typedef NS_ENUM(NSInteger, SNTRuleCleanup) {
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
enum class FileAccessPolicyDecision {
|
||||
kNoPolicy,
|
||||
kDenied,
|
||||
@@ -190,6 +191,19 @@ enum class FileAccessPolicyDecision {
|
||||
kAllowedReadAccess,
|
||||
kAllowedAuditOnly,
|
||||
};
|
||||
|
||||
enum class StatChangeStep {
|
||||
kNoChange = 0,
|
||||
kMessageCreate,
|
||||
kCodesignValidation,
|
||||
};
|
||||
|
||||
enum class StatResult {
|
||||
kOK = 0,
|
||||
kStatError,
|
||||
kDevnoInodeMismatch,
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
static const char *kSantaDPath =
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -380,23 +380,16 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
NSError *err;
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err];
|
||||
|
||||
NSString *cdhash =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoUnique];
|
||||
NSString *teamID =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
|
||||
NSString *identifier =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
NSString *cdhash = csc.cdhash;
|
||||
NSString *teamID = csc.teamID;
|
||||
NSString *identifier = csc.signingID;
|
||||
|
||||
NSString *signingID;
|
||||
if (identifier) {
|
||||
if (teamID) {
|
||||
signingID = [NSString stringWithFormat:@"%@:%@", teamID, identifier];
|
||||
} else {
|
||||
id platformID =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
|
||||
if ([platformID isKindOfClass:[NSNumber class]] && [platformID intValue] != 0) {
|
||||
signingID = [NSString stringWithFormat:@"platform:%@", identifier];
|
||||
}
|
||||
} else if (csc.platformBinary) {
|
||||
signingID = [NSString stringWithFormat:@"platform:%@", identifier];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,21 +515,30 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
- (SNTAttributeBlock)teamID {
|
||||
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
|
||||
return [csc.signingInformation valueForKey:@"teamid"];
|
||||
return csc.teamID;
|
||||
};
|
||||
}
|
||||
|
||||
- (SNTAttributeBlock)signingID {
|
||||
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
|
||||
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
|
||||
NSString *identifier = csc.signingID;
|
||||
NSString *teamID = csc.teamID;
|
||||
if (!identifier) return nil;
|
||||
if (teamID) {
|
||||
return [NSString stringWithFormat:@"%@:%@", teamID, identifier];
|
||||
} else if (csc.platformBinary) {
|
||||
return [NSString stringWithFormat:@"platform:%@", identifier];
|
||||
}
|
||||
return nil;
|
||||
};
|
||||
}
|
||||
|
||||
- (SNTAttributeBlock)cdhash {
|
||||
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
|
||||
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoUnique];
|
||||
return csc.cdhash;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -61,7 +61,8 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@"\n"
|
||||
@" One of:\n"
|
||||
@" --path {path}: path of binary/bundle to add/remove.\n"
|
||||
@" Will add the hash of the file currently at that path.\n"
|
||||
@" Will add an appropriate rule for the file currently at that path.\n"
|
||||
@" Defaults to a SHA-256 rule unless overridden with another flag.\n"
|
||||
@" Does not work with --check. Use the fileinfo verb to check.\n"
|
||||
@" the rule state of a file.\n"
|
||||
@" --identifier {sha256|teamID|signingID|cdhash}: identifier to add/remove/check\n"
|
||||
@@ -259,10 +260,17 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
newRule.identifier = cs.leafCertificate.SHA256;
|
||||
} else if (newRule.type == SNTRuleTypeCDHash) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.identifier =
|
||||
[cs.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
} else if (newRule.type == SNTRuleTypeTeamID || newRule.type == SNTRuleTypeSigningID) {
|
||||
// noop
|
||||
newRule.identifier = cs.cdhash;
|
||||
} else if (newRule.type == SNTRuleTypeTeamID) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.identifier = cs.teamID;
|
||||
} else if (newRule.type == SNTRuleTypeSigningID) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
if (cs.teamID.length) {
|
||||
newRule.identifier = [NSString stringWithFormat:@"%@:%@", cs.teamID, cs.signingID];
|
||||
} else if (cs.platformBinary) {
|
||||
newRule.identifier = [NSString stringWithFormat:@"platform:%@", cs.signingID];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +299,8 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
if (newRule.state == SNTRuleStateUnknown) {
|
||||
[self printErrorUsageAndExit:@"No state specified"];
|
||||
} else if (!newRule.identifier) {
|
||||
[self printErrorUsageAndExit:@"Either SHA-256, team ID, or path to file must be specified"];
|
||||
[self printErrorUsageAndExit:
|
||||
@"A valid SHA-256, CDHash, Signing ID, team ID, or path to file must be specified"];
|
||||
}
|
||||
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
|
||||
@@ -193,7 +193,7 @@ objc_library(
|
||||
|
||||
objc_library(
|
||||
name = "SNTPolicyProcessor",
|
||||
srcs = ["SNTPolicyProcessor.m"],
|
||||
srcs = ["SNTPolicyProcessor.mm"],
|
||||
hdrs = ["SNTPolicyProcessor.h"],
|
||||
deps = [
|
||||
":SNTRuleTable",
|
||||
@@ -209,6 +209,19 @@ objc_library(
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
"@MOLXPCConnection",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTPolicyProcessorTest",
|
||||
srcs = ["SNTPolicyProcessorTest.mm"],
|
||||
deps = [
|
||||
":SNTPolicyProcessor",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:TestUtils",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -645,6 +658,7 @@ objc_library(
|
||||
deps = [
|
||||
":EndpointSecurityClient",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
],
|
||||
)
|
||||
@@ -705,6 +719,7 @@ objc_library(
|
||||
srcs = ["Metrics.mm"],
|
||||
hdrs = ["Metrics.h"],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
":SNTApplicationCoreMetrics",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
@@ -779,6 +794,7 @@ objc_library(
|
||||
"//Source/common:SNTXPCUnprivilegedControlInterface",
|
||||
"//Source/common:Unit",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
"//Source/santad/ProcessTree/annotations:originator",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
@@ -912,9 +928,6 @@ santa_unit_test(
|
||||
santa_unit_test(
|
||||
name = "SantadTest",
|
||||
srcs = ["SantadTest.mm"],
|
||||
data = [
|
||||
"//Source/santad/testdata:binaryrules_testdata",
|
||||
],
|
||||
minimum_os_version = "11.0",
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
@@ -923,6 +936,9 @@ santa_unit_test(
|
||||
sdk_frameworks = [
|
||||
"DiskArbitration",
|
||||
],
|
||||
structured_resources = [
|
||||
"//Source/santad/testdata:binaryrules_testdata",
|
||||
],
|
||||
tags = ["exclusive"],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
@@ -1014,7 +1030,7 @@ santa_unit_test(
|
||||
santa_unit_test(
|
||||
name = "EndpointSecuritySerializerProtobufTest",
|
||||
srcs = ["Logs/EndpointSecurity/Serializers/ProtobufTest.mm"],
|
||||
data = [
|
||||
structured_resources = [
|
||||
"//Source/santad/testdata:protobuf_json_testdata",
|
||||
],
|
||||
deps = [
|
||||
@@ -1169,7 +1185,10 @@ santa_unit_test(
|
||||
name = "MetricsTest",
|
||||
srcs = ["MetricsTest.mm"],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
@@ -1416,10 +1435,13 @@ test_suite(
|
||||
":SNTEndpointSecurityTamperResistanceTest",
|
||||
":SNTEventTableTest",
|
||||
":SNTExecutionControllerTest",
|
||||
":SNTPolicyProcessorTest",
|
||||
":SNTRuleTableTest",
|
||||
":SantadTest",
|
||||
":WatchItemsTest",
|
||||
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool_test",
|
||||
"//Source/santad/ProcessTree:process_tree_test",
|
||||
"//Source/santad/ProcessTree/annotations:originator_test",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
@@ -152,13 +152,13 @@ void DARegisterDiskAppearedCallback(DASessionRef session, CFDictionaryRef __null
|
||||
|
||||
void DARegisterDiskDisappearedCallback(DASessionRef session, CFDictionaryRef __nullable match,
|
||||
DADiskDisappearedCallback callback,
|
||||
void *__nullable context) {};
|
||||
void *__nullable context){};
|
||||
|
||||
void DARegisterDiskDescriptionChangedCallback(DASessionRef session,
|
||||
CFDictionaryRef __nullable match,
|
||||
CFArrayRef __nullable watch,
|
||||
DADiskDescriptionChangedCallback callback,
|
||||
void *__nullable context) {};
|
||||
void *__nullable context){};
|
||||
|
||||
void DASessionSetDispatchQueue(DASessionRef session, dispatch_queue_t __nullable queue) {
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EndpointSecurityAPI;
|
||||
class MessagePeer;
|
||||
|
||||
class Message {
|
||||
public:
|
||||
@@ -53,12 +55,22 @@ class Message {
|
||||
|
||||
std::string ParentProcessName() const;
|
||||
|
||||
void UpdateStatState(enum StatChangeStep step) const;
|
||||
|
||||
inline StatChangeStep StatChangeStep() const { return stat_change_step_; }
|
||||
inline StatResult StatResult() const { return stat_result_; }
|
||||
|
||||
friend class santa::santad::event_providers::endpoint_security::MessagePeer;
|
||||
|
||||
private:
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi_;
|
||||
const es_message_t* es_msg_;
|
||||
std::optional<process_tree::ProcessToken> process_token_;
|
||||
|
||||
std::string GetProcessName(pid_t pid) const;
|
||||
|
||||
mutable enum StatChangeStep stat_change_step_ = StatChangeStep::kNoChange;
|
||||
mutable enum StatResult stat_result_ = StatResult::kOK;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libproc.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
|
||||
@@ -24,6 +25,7 @@ namespace santa::santad::event_providers::endpoint_security {
|
||||
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
|
||||
: esapi_(std::move(esapi)), es_msg_(es_msg), process_token_(std::nullopt) {
|
||||
esapi_->RetainMessage(es_msg);
|
||||
UpdateStatState(StatChangeStep::kMessageCreate);
|
||||
}
|
||||
|
||||
Message::~Message() {
|
||||
@@ -38,6 +40,8 @@ Message::Message(Message &&other) {
|
||||
other.es_msg_ = nullptr;
|
||||
process_token_ = std::move(other.process_token_);
|
||||
other.process_token_ = std::nullopt;
|
||||
stat_change_step_ = other.stat_change_step_;
|
||||
stat_result_ = other.stat_result_;
|
||||
}
|
||||
|
||||
Message::Message(const Message &other) {
|
||||
@@ -45,6 +49,27 @@ Message::Message(const Message &other) {
|
||||
es_msg_ = other.es_msg_;
|
||||
esapi_->RetainMessage(es_msg_);
|
||||
process_token_ = other.process_token_;
|
||||
stat_change_step_ = other.stat_change_step_;
|
||||
stat_result_ = other.stat_result_;
|
||||
}
|
||||
|
||||
void Message::UpdateStatState(enum StatChangeStep step) const {
|
||||
// Only update state for AUTH EXEC events and if no previous change was detected
|
||||
if (es_msg_->event_type == ES_EVENT_TYPE_AUTH_EXEC &&
|
||||
stat_change_step_ == StatChangeStep::kNoChange &&
|
||||
// Note: The following checks are required due to tests that only
|
||||
// partially construct an es_message_t.
|
||||
es_msg_->event.exec.target && es_msg_->event.exec.target->executable) {
|
||||
struct stat &es_sb = es_msg_->event.exec.target->executable->stat;
|
||||
struct stat sb;
|
||||
int ret = stat(es_msg_->event.exec.target->executable->path.data, &sb);
|
||||
// If stat failed, or if devno/inode changed, update state.
|
||||
if (ret != 0 || es_sb.st_ino != sb.st_ino || es_sb.st_dev != sb.st_dev) {
|
||||
stat_change_step_ = step;
|
||||
// Determine the specific condition that failed for tracking purposes
|
||||
stat_result_ = (ret != 0) ? StatResult::kStatError : StatResult::kDevnoInodeMismatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Message::SetProcessToken(process_tree::ProcessToken tok) {
|
||||
|
||||
@@ -92,7 +92,7 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
_notifyQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.notify_queue",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_BACKGROUND, 0));
|
||||
QOS_CLASS_UTILITY, 0));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -146,12 +146,10 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
// sequence numbers are processed in order.
|
||||
self->_metrics->UpdateEventStats(self->_processor, esMsg.operator->());
|
||||
|
||||
es_event_type_t eventType = esMsg->event_type;
|
||||
|
||||
if ([self handleContextMessage:esMsg]) {
|
||||
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
self->_metrics->SetEventMetrics(self->_processor, eventType, EventDisposition::kProcessed,
|
||||
processingEnd - processingStart);
|
||||
self->_metrics->SetEventMetrics(self->_processor, EventDisposition::kProcessed,
|
||||
processingEnd - processingStart, esMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,13 +157,13 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
[self handleMessage:std::move(esMsg)
|
||||
recordEventMetrics:^(EventDisposition disposition) {
|
||||
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
self->_metrics->SetEventMetrics(self->_processor, eventType, disposition,
|
||||
processingEnd - processingStart);
|
||||
self->_metrics->SetEventMetrics(self->_processor, disposition,
|
||||
processingEnd - processingStart, esMsg);
|
||||
}];
|
||||
} else {
|
||||
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
self->_metrics->SetEventMetrics(self->_processor, eventType, EventDisposition::kDropped,
|
||||
processingEnd - processingStart);
|
||||
self->_metrics->SetEventMetrics(self->_processor, EventDisposition::kDropped,
|
||||
processingEnd - processingStart, esMsg);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -387,16 +387,16 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
// Create mock disks with desired args
|
||||
MockDADisk * (^CreateMockDisk)(NSString *, NSString *) =
|
||||
^MockDADisk *(NSString *mountOn, NSString *mountFrom) {
|
||||
MockDADisk *mockDisk = [[MockDADisk alloc] init];
|
||||
mockDisk.diskDescription = @{
|
||||
@"DAVolumePath" : mountOn, // f_mntonname,
|
||||
@"DADevicePath" : mountOn, // f_mntonname,
|
||||
@"DAMediaBSDName" : mountFrom, // f_mntfromname,
|
||||
};
|
||||
|
||||
return mockDisk;
|
||||
MockDADisk *mockDisk = [[MockDADisk alloc] init];
|
||||
mockDisk.diskDescription = @{
|
||||
@"DAVolumePath" : mountOn, // f_mntonname,
|
||||
@"DADevicePath" : mountOn, // f_mntonname,
|
||||
@"DAMediaBSDName" : mountFrom, // f_mntfromname,
|
||||
};
|
||||
|
||||
return mockDisk;
|
||||
};
|
||||
|
||||
// Reset the Mock DA property, setup disks and remount args, then trigger the test
|
||||
void (^PerformStartupTest)(NSArray<MockDADisk *> *, NSArray<NSString *> *,
|
||||
SNTDeviceManagerStartupPreferences) =
|
||||
|
||||
@@ -381,7 +381,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
|
||||
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, struct timespec event_time,
|
||||
struct timespec processed_time) {
|
||||
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
|
||||
::pbv1::SantaMessage *santa_msg = Arena::Create<::pbv1::SantaMessage>(arena);
|
||||
|
||||
if (EnabledMachineID()) {
|
||||
EncodeString([santa_msg] { return santa_msg->mutable_machine_id(); }, MachineID());
|
||||
@@ -415,7 +415,7 @@ std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
|
||||
// TODO: Profile this. It's probably not the most efficient way to do this.
|
||||
JsonPrintOptions options;
|
||||
options.always_print_enums_as_ints = false;
|
||||
options.always_print_primitive_fields = true;
|
||||
options.always_print_fields_with_no_presence = true;
|
||||
options.preserve_proto_field_names = true;
|
||||
std::string json;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "process",
|
||||
hdrs = ["process.h"],
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [
|
||||
"//Source/santad/ProcessTree/annotations:annotator",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
@@ -26,7 +29,6 @@ objc_library(
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [
|
||||
":process",
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
@@ -42,12 +44,10 @@ objc_library(
|
||||
proto_library(
|
||||
name = "process_tree_proto",
|
||||
srcs = ["process_tree.proto"],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "process_tree_cc_proto",
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [":process_tree_proto"],
|
||||
)
|
||||
|
||||
@@ -58,7 +58,6 @@ objc_library(
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [
|
||||
":process_tree",
|
||||
"//Source/santad:EndpointSecurityAPI",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
)
|
||||
@@ -9,3 +11,27 @@ cc_library(
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "originator",
|
||||
srcs = ["originator.cc"],
|
||||
hdrs = ["originator.h"],
|
||||
deps = [
|
||||
":annotator",
|
||||
"//Source/santad/ProcessTree:process",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "originator_test",
|
||||
srcs = ["originator_test.mm"],
|
||||
deps = [
|
||||
":originator",
|
||||
"//Source/santad/ProcessTree:process",
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
"//Source/santad/ProcessTree:process_tree_test_helpers",
|
||||
],
|
||||
)
|
||||
|
||||
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
|
||||
@@ -3,4 +3,11 @@ syntax = "proto3";
|
||||
package santa.pb.v1.process_tree;
|
||||
|
||||
message Annotations {
|
||||
enum Originator {
|
||||
UNSPECIFIED = 0;
|
||||
LOGIN = 1;
|
||||
CRON = 2;
|
||||
}
|
||||
|
||||
Originator originator = 1;
|
||||
}
|
||||
|
||||
@@ -256,9 +256,11 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
// TODO(markowsky): Maybe add a metric here for how many large executables we're seeing.
|
||||
// if (binInfo.fileSize > SomeUpperLimit) ...
|
||||
|
||||
SNTCachedDecision *cd = [self.policyProcessor
|
||||
decisionForFileInfo:binInfo
|
||||
targetProcess:targetProc
|
||||
SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo
|
||||
targetProcess:targetProc
|
||||
preCodesignCheckCallback:^(void) {
|
||||
esMsg.UpdateStatState(StatChangeStep::kCodesignValidation);
|
||||
}
|
||||
entitlementsFilterCallback:^NSDictionary *(const char *teamID, NSDictionary *entitlements) {
|
||||
if (!entitlements) {
|
||||
return nil;
|
||||
|
||||
@@ -387,9 +387,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
}
|
||||
|
||||
- (void)testTeamIDAllowRule {
|
||||
OCMStub([self.mockCodesignChecker signingInformation]).andReturn((@{
|
||||
(__bridge NSString *)kSecCodeInfoTeamIdentifier : @(kExampleTeamID),
|
||||
}));
|
||||
OCMStub([self.mockCodesignChecker teamID]).andReturn(@(kExampleTeamID));
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
@@ -405,9 +403,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
}
|
||||
|
||||
- (void)testTeamIDBlockRule {
|
||||
OCMStub([self.mockCodesignChecker signingInformation]).andReturn((@{
|
||||
(__bridge NSString *)kSecCodeInfoTeamIdentifier : @(kExampleTeamID),
|
||||
}));
|
||||
OCMStub([self.mockCodesignChecker teamID]).andReturn(@(kExampleTeamID));
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
|
||||
@class MOLCodesignChecker;
|
||||
@@ -50,6 +51,13 @@
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback;
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc
|
||||
preCodesignCheckCallback:(void (^_Nullable)(void))preCodesignCheckCallback
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback;
|
||||
|
||||
///
|
||||
/// A wrapper for decisionForFileInfo:fileSHA256:certificateSHA256:. This method is slower as it
|
||||
@@ -60,4 +68,12 @@
|
||||
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
|
||||
identifiers:(nonnull SNTRuleIdentifiers *)identifiers;
|
||||
|
||||
///
|
||||
/// Updates a decision for a given file and agent configuration.
|
||||
///
|
||||
/// Returns YES if the decision requires no futher processing NO otherwise.
|
||||
- (BOOL)decision:(nonnull SNTCachedDecision *)cd
|
||||
forRule:(nonnull SNTRule *)rule
|
||||
withTransitiveRules:(BOOL)transitive;
|
||||
|
||||
@end
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
|
||||
@interface SNTPolicyProcessor ()
|
||||
@property SNTRuleTable *ruleTable;
|
||||
@@ -44,37 +45,158 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
cdhash:(nullable NSString *)cdhash
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID
|
||||
isProdSignedCallback:(BOOL (^_Nonnull)())isProdSignedCallback
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nullable)(
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.cdhash = cdhash;
|
||||
cd.sha256 = fileSHA256 ?: fileInfo.SHA256;
|
||||
cd.teamID = teamID;
|
||||
cd.signingID = signingID;
|
||||
// This method applies the rules to the cached decision object.
|
||||
//
|
||||
// It returns YES if the decision was made, NO if the decision was not made.
|
||||
- (BOOL)decision:(SNTCachedDecision *)cd
|
||||
forRule:(SNTRule *)rule
|
||||
withTransitiveRules:(BOOL)enableTransitiveRules {
|
||||
static const auto decisions =
|
||||
absl::flat_hash_map<std::pair<SNTRuleType, SNTRuleState>, SNTEventState>{
|
||||
{{SNTRuleTypeCDHash, SNTRuleStateAllow}, SNTEventStateAllowCDHash},
|
||||
{{SNTRuleTypeCDHash, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler},
|
||||
{{SNTRuleTypeCDHash, SNTRuleStateBlock}, SNTEventStateBlockCDHash},
|
||||
{{SNTRuleTypeCDHash, SNTRuleStateSilentBlock}, SNTEventStateBlockCDHash},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateAllow}, SNTEventStateAllowBinary},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateAllowTransitive}, SNTEventStateAllowTransitive},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateSilentBlock}, SNTEventStateBlockBinary},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateBlock}, SNTEventStateBlockBinary},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateAllow}, SNTEventStateAllowSigningID},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateSilentBlock}, SNTEventStateBlockSigningID},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateBlock}, SNTEventStateBlockSigningID},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateAllow}, SNTEventStateAllowCertificate},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateSilentBlock}, SNTEventStateBlockCertificate},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateBlock}, SNTEventStateBlockCertificate},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateAllow}, SNTEventStateAllowTeamID},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateSilentBlock}, SNTEventStateBlockTeamID},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateBlock}, SNTEventStateBlockTeamID},
|
||||
};
|
||||
|
||||
auto iterator = decisions.find(std::pair<SNTRuleType, SNTRuleState>{rule.type, rule.state});
|
||||
if (iterator != decisions.end()) {
|
||||
cd.decision = iterator->second;
|
||||
} else {
|
||||
// If we have an invalid state combination then either we have stale data in
|
||||
// the database or a programming error. We treat this as if the
|
||||
// corresponding rule was not found.
|
||||
LOGE(@"Invalid rule type/state combination %ld/%ld", rule.type, rule.state);
|
||||
return NO;
|
||||
}
|
||||
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateSilentBlock: cd.silentBlock = YES; break;
|
||||
case SNTRuleStateAllowCompiler:
|
||||
if (!enableTransitiveRules) {
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeCDHash: cd.decision = SNTEventStateAllowCDHash; break;
|
||||
case SNTRuleTypeBinary: cd.decision = SNTEventStateAllowBinary; break;
|
||||
case SNTRuleTypeSigningID: cd.decision = SNTEventStateAllowSigningID; break;
|
||||
default:
|
||||
// Programming error. Something's marked as a compiler that shouldn't
|
||||
// be.
|
||||
LOGE(@"Invalid compiler rule type %ld", rule.type);
|
||||
[NSException
|
||||
raise:@"Invalid compiler rule type"
|
||||
format:@"decision:forRule:withTransitiveRules: Unexpected compiler rule type: %ld",
|
||||
rule.type];
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SNTRuleStateAllowTransitive:
|
||||
// If transitive rules are disabled, then we treat
|
||||
// SNTRuleStateAllowTransitive rules as if a matching rule was not found
|
||||
// and set the state to unknown. Otherwise the decision map will have already set
|
||||
// the EventState to SNTEventStateAllowTransitive.
|
||||
if (!enableTransitiveRules) {
|
||||
cd.decision = SNTEventStateUnknown;
|
||||
return NO;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// If its not one of the special cases above, we don't need to do anything.
|
||||
break;
|
||||
}
|
||||
|
||||
// We know we have a match so apply the custom messages
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
static void UpdateCachedDecisionSigningInfo(
|
||||
SNTCachedDecision *cd, MOLCodesignChecker *csInfo,
|
||||
NSDictionary *_Nullable (^entitlementsFilterCallback)(NSDictionary *_Nullable entitlements)) {
|
||||
cd.certSHA256 = csInfo.leafCertificate.SHA256;
|
||||
cd.certCommonName = csInfo.leafCertificate.commonName;
|
||||
cd.certChain = csInfo.certificates;
|
||||
// Check if we need to get teamID from code signing.
|
||||
if (!cd.teamID) {
|
||||
cd.teamID = csInfo.teamID;
|
||||
}
|
||||
|
||||
// Ensure that if no teamID exists that the signing info confirms it is a
|
||||
// platform binary. If not, remove the signingID.
|
||||
if (!cd.teamID && cd.signingID) {
|
||||
if (!csInfo.platformBinary) {
|
||||
cd.signingID = nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *entitlements = csInfo.entitlements;
|
||||
|
||||
if (entitlementsFilterCallback) {
|
||||
cd.entitlements = entitlementsFilterCallback(entitlements);
|
||||
cd.entitlementsFiltered = (cd.entitlements.count != entitlements.count);
|
||||
} else {
|
||||
cd.entitlements = [entitlements sntDeepCopy];
|
||||
cd.entitlementsFiltered = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)
|
||||
decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
cdhash:(nullable NSString *)cdhash
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID
|
||||
isProdSignedCallback:(BOOL (^_Nonnull)())isProdSignedCallback
|
||||
entitlementsFilterCallback:(NSDictionary *_Nullable (^_Nullable)(
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback
|
||||
preCodesignCheckCallback:(void (^_Nullable)(void))preCodesignCheckCallback {
|
||||
// Check the hash before allocating a SNTCachedDecision.
|
||||
NSString *fileHash = fileSHA256 ?: fileInfo.SHA256;
|
||||
SNTClientMode mode = [self.configurator clientMode];
|
||||
cd.decisionClientMode = mode;
|
||||
|
||||
// If the binary is a critical system binary, don't check its signature.
|
||||
// The binary was validated at startup when the rule table was initialized.
|
||||
SNTCachedDecision *systemCd = self.ruleTable.criticalSystemBinaries[cd.sha256];
|
||||
SNTCachedDecision *systemCd = self.ruleTable.criticalSystemBinaries[fileHash];
|
||||
if (systemCd) {
|
||||
systemCd.decisionClientMode = mode;
|
||||
return systemCd;
|
||||
}
|
||||
|
||||
// Allocate a new cached decision for the execution.
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.cdhash = cdhash;
|
||||
cd.sha256 = fileHash;
|
||||
cd.teamID = teamID;
|
||||
cd.signingID = signingID;
|
||||
cd.decisionClientMode = mode;
|
||||
cd.quarantineURL = fileInfo.quarantineDataURL;
|
||||
|
||||
NSError *csInfoError;
|
||||
if (certificateSHA256.length) {
|
||||
cd.certSHA256 = certificateSHA256;
|
||||
} else {
|
||||
if (preCodesignCheckCallback) {
|
||||
preCodesignCheckCallback();
|
||||
}
|
||||
|
||||
// Grab the code signature, if there's an error don't try to capture
|
||||
// any of the signature details. Also clear out any rule lookup parameters
|
||||
// that would require being validly signed.
|
||||
@@ -87,36 +209,9 @@
|
||||
cd.signingID = nil;
|
||||
cd.cdhash = nil;
|
||||
} else {
|
||||
cd.certSHA256 = csInfo.leafCertificate.SHA256;
|
||||
cd.certCommonName = csInfo.leafCertificate.commonName;
|
||||
cd.certChain = csInfo.certificates;
|
||||
cd.teamID = teamID
|
||||
?: [csInfo.signingInformation
|
||||
objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
|
||||
|
||||
// Ensure that if no teamID exists that the signing info confirms it is a
|
||||
// platform binary. If not, remove the signingID.
|
||||
if (!cd.teamID && cd.signingID) {
|
||||
id platformID = [csInfo.signingInformation
|
||||
objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
|
||||
if (![platformID isKindOfClass:[NSNumber class]] || [platformID intValue] == 0) {
|
||||
cd.signingID = nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *entitlements =
|
||||
csInfo.signingInformation[(__bridge NSString *)kSecCodeInfoEntitlementsDict];
|
||||
|
||||
if (entitlementsFilterCallback) {
|
||||
cd.entitlements = entitlementsFilterCallback(entitlements);
|
||||
cd.entitlementsFiltered = (cd.entitlements.count == entitlements.count);
|
||||
} else {
|
||||
cd.entitlements = [entitlements sntDeepCopy];
|
||||
cd.entitlementsFiltered = NO;
|
||||
}
|
||||
UpdateCachedDecisionSigningInfo(cd, csInfo, entitlementsFilterCallback);
|
||||
}
|
||||
}
|
||||
cd.quarantineURL = fileInfo.quarantineDataURL;
|
||||
|
||||
// Do not evaluate TeamID/SigningID rules for dev-signed code based on the
|
||||
// assumption that orgs are generally more relaxed about dev signed cert
|
||||
@@ -138,116 +233,11 @@
|
||||
.certificateSHA256 = cd.certSHA256,
|
||||
.teamID = cd.teamID}];
|
||||
if (rule) {
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeCDHash:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCDHash; return cd;
|
||||
case SNTRuleStateAllowCompiler:
|
||||
// If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules
|
||||
// become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if
|
||||
// it were SNTRuleStateAllowCDHash.
|
||||
if ([self.configurator enableTransitiveRules]) {
|
||||
cd.decision = SNTEventStateAllowCompiler;
|
||||
} else {
|
||||
cd.decision = SNTEventStateAllowCDHash;
|
||||
}
|
||||
return cd;
|
||||
case SNTRuleStateSilentBlock:
|
||||
cd.silentBlock = YES;
|
||||
// intentional fallthrough
|
||||
case SNTRuleStateBlock:
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
cd.decision = SNTEventStateBlockCDHash;
|
||||
return cd;
|
||||
default: break;
|
||||
}
|
||||
case SNTRuleTypeBinary:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowBinary; return cd;
|
||||
case SNTRuleStateSilentBlock: cd.silentBlock = YES;
|
||||
case SNTRuleStateBlock:
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
cd.decision = SNTEventStateBlockBinary;
|
||||
return cd;
|
||||
case SNTRuleStateAllowCompiler:
|
||||
// If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules
|
||||
// become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if
|
||||
// it were SNTRuleStateAllow.
|
||||
if ([self.configurator enableTransitiveRules]) {
|
||||
cd.decision = SNTEventStateAllowCompiler;
|
||||
} else {
|
||||
cd.decision = SNTEventStateAllowBinary;
|
||||
}
|
||||
return cd;
|
||||
case SNTRuleStateAllowTransitive:
|
||||
// If transitive rules are enabled, then SNTRuleStateAllowTransitive
|
||||
// rules become SNTEventStateAllowTransitive decisions. Otherwise, we treat the
|
||||
// rule as if it were SNTRuleStateUnknown.
|
||||
if ([self.configurator enableTransitiveRules]) {
|
||||
cd.decision = SNTEventStateAllowTransitive;
|
||||
return cd;
|
||||
} else {
|
||||
rule.state = SNTRuleStateUnknown;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case SNTRuleTypeSigningID:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowSigningID; return cd;
|
||||
case SNTRuleStateAllowCompiler:
|
||||
// If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules
|
||||
// become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if
|
||||
// it were SNTRuleStateAllowSigningID.
|
||||
if ([self.configurator enableTransitiveRules]) {
|
||||
cd.decision = SNTEventStateAllowCompiler;
|
||||
} else {
|
||||
cd.decision = SNTEventStateAllowSigningID;
|
||||
}
|
||||
return cd;
|
||||
case SNTRuleStateSilentBlock:
|
||||
cd.silentBlock = YES;
|
||||
// intentional fallthrough
|
||||
case SNTRuleStateBlock:
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
cd.decision = SNTEventStateBlockSigningID;
|
||||
return cd;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case SNTRuleTypeCertificate:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCertificate; return cd;
|
||||
case SNTRuleStateSilentBlock:
|
||||
cd.silentBlock = YES;
|
||||
// intentional fallthrough
|
||||
case SNTRuleStateBlock:
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
cd.decision = SNTEventStateBlockCertificate;
|
||||
return cd;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case SNTRuleTypeTeamID:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowTeamID; return cd;
|
||||
case SNTRuleStateSilentBlock:
|
||||
cd.silentBlock = YES;
|
||||
// intentional fallthrough
|
||||
case SNTRuleStateBlock:
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
cd.decision = SNTEventStateBlockTeamID;
|
||||
return cd;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
|
||||
default: break;
|
||||
// If we have a rule match we don't need to process any further.
|
||||
if ([self decision:cd
|
||||
forRule:rule
|
||||
withTransitiveRules:self.configurator.enableTransitiveRules]) {
|
||||
return cd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +276,19 @@
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
targetProcess:targetProc
|
||||
preCodesignCheckCallback:nil
|
||||
entitlementsFilterCallback:entitlementsFilterCallback];
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc
|
||||
preCodesignCheckCallback:(void (^_Nullable)(void))preCodesignCheckCallback
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
NSString *signingID;
|
||||
NSString *teamID;
|
||||
NSString *cdhash;
|
||||
@@ -334,7 +337,8 @@
|
||||
}
|
||||
entitlementsFilterCallback:^NSDictionary *(NSDictionary *entitlements) {
|
||||
return entitlementsFilterCallback(entitlementsFilterTeamID, entitlements);
|
||||
}];
|
||||
}
|
||||
preCodesignCheckCallback:preCodesignCheckCallback];
|
||||
}
|
||||
|
||||
// Used by `$ santactl fileinfo`.
|
||||
@@ -371,7 +375,8 @@
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
entitlementsFilterCallback:nil];
|
||||
entitlementsFilterCallback:nil
|
||||
preCodesignCheckCallback:nil];
|
||||
}
|
||||
|
||||
///
|
||||
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
|
||||
@@ -24,6 +24,7 @@
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#include "Source/santad/DataLayer/WatchItems.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/ProcessTree/annotations/originator.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
#include "Source/santad/SNTDecisionCache.h"
|
||||
@@ -159,8 +160,11 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
|
||||
std::vector<std::unique_ptr<process_tree::Annotator>> annotators;
|
||||
|
||||
for (NSString *annotation in [configurator enabledProcessAnnotations]) {
|
||||
// TODO(nickmg): add annotation name switch
|
||||
(void)annotation;
|
||||
if ([[annotation lowercaseString] isEqualToString:@"originator"]) {
|
||||
annotators.emplace_back(std::make_unique<process_tree::OriginatorAnnotator>());
|
||||
} else {
|
||||
LOGW(@"Unrecognized process annotation %@", annotation);
|
||||
}
|
||||
}
|
||||
|
||||
auto tree_status = process_tree::CreateTree(std::move(annotators));
|
||||
|
||||
@@ -71,7 +71,6 @@ static void SetBinaryDataFromHexString(const char *hexStr, uint8_t *buf, size_t
|
||||
}
|
||||
}
|
||||
|
||||
static NSString *const testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
static const char *kAllowedSigningID = "com.google.allowed_signing_id";
|
||||
static const char *kBlockedSigningID = "com.google.blocked_signing_id";
|
||||
static const char *kNoRuleMatchSigningID = "com.google.no_rule_match_signing_id";
|
||||
@@ -127,7 +126,8 @@ static const char *kBlockedCDHash = "7218eddfee4d3eba4873dedf22d1391d79aea25f";
|
||||
OCMStub([mockConfigurator fileAccessPolicyUpdateIntervalSec]).andReturn(600);
|
||||
|
||||
NSString *testPath = [NSString pathWithComponents:@[
|
||||
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testBinariesPath
|
||||
[[NSBundle bundleForClass:[self class]] resourcePath],
|
||||
@"binaryrules",
|
||||
]];
|
||||
|
||||
OCMStub([self.mockSNTDatabaseController databasePath]).andReturn(testPath);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
GIT_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
find $GIT_ROOT \( -name "*.m" -o -name "*.h" -o -name "*.mm" -o -name "*.cc" \) -exec xcrun clang-format -i {} \+
|
||||
buildifier --lint=fix -r $GIT_ROOT
|
||||
find ${GIT_ROOT} \( -name "*.m" -o -name "*.h" -o -name "*.mm" -o -name "*.cc" \) -exec xcrun clang-format -i {} \+
|
||||
buildifier --lint=fix -r ${GIT_ROOT}
|
||||
python3 -m pyink --config ${GIT_ROOT}/.pyink-config ${GIT_ROOT}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
load("//:helper.bzl", "run_command", "santa_unit_test")
|
||||
|
||||
package(default_visibility = ["//:santa_package_group"])
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@end
|
||||
|
||||
int main(int argc, const char *argv[]) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: %s bundle_path [usb_disk]", argv[0]);
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "Usage: %s bundle_path runner_disk [usb_disk]", argv[0]);
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
@@ -50,14 +50,16 @@ int main(int argc, const char *argv[]) {
|
||||
bundleDir = [bundleDir stringByAppendingString:@"/"];
|
||||
}
|
||||
|
||||
NSString *usbDisk;
|
||||
if (argc > 2) {
|
||||
usbDisk = @(argv[2]);
|
||||
NSString *runnerDisk = @(argv[2]);
|
||||
|
||||
NSString *usbDisk = NULL;
|
||||
if (argc > 3) {
|
||||
usbDisk = @(argv[3]);
|
||||
}
|
||||
|
||||
VZVirtualMachine *vm =
|
||||
[MacOSVirtualMachineConfigurationHelper createVirtualMachineWithBundleDir:bundleDir
|
||||
roDisk:nil
|
||||
roDisk:runnerDisk
|
||||
usbDisk:usbDisk];
|
||||
|
||||
MacOSVirtualMachineDelegate *delegate = [MacOSVirtualMachineDelegate new];
|
||||
|
||||
@@ -33,16 +33,23 @@ fi
|
||||
# Install rosetta (for test binaries)
|
||||
softwareupdate --install-rosetta --agree-to-license
|
||||
|
||||
# Install actions runner
|
||||
mkdir ~/actions-runner
|
||||
pushd ~/actions-runner
|
||||
curl -o actions-runner-osx-arm64-2.296.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.296.0/actions-runner-osx-arm64-2.296.0.tar.gz
|
||||
echo 'e358086b924d2e8d8abf50beec57ee7a3bb0c7d412f13abc51380f1b1894d776 actions-runner-osx-arm64-2.296.0.tar.gz' | shasum -a 256 -c
|
||||
tar xzf ./actions-runner-osx-arm64-2.296.0.tar.gz
|
||||
./config.sh --url https://github.com/google/santa
|
||||
./svc.sh install
|
||||
./svc.sh start
|
||||
popd
|
||||
# Add a LaunchAgent to start the mounted runner
|
||||
tee ${HOME}/Library/LaunchAgents/runner.plist << EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.google.santa.e2erunner</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Volumes/init/run.sh</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# Run sample applescript to grant bash accessibility and automation control
|
||||
clang "${SCRIPT_DIR}/disclaim.c" -o /tmp/disclaim
|
||||
|
||||
@@ -1,121 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download and run the given Santa E2E testing VM image."""
|
||||
import datetime
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.request
|
||||
|
||||
from google.cloud import storage
|
||||
from google.oauth2 import service_account
|
||||
|
||||
PROJECT = "santa-e2e"
|
||||
SA_KEY = "/opt/santa-e2e-sa.json"
|
||||
BUCKET = "santa-e2e-vms"
|
||||
COSIGN = "/opt/bin/cosign"
|
||||
PUBKEY = "/opt/santa-e2e-vm-signer.pub"
|
||||
VMCLI = "/opt/bin/VMCLI"
|
||||
VMS_DIR = pathlib.Path.home() / "VMs"
|
||||
TIMEOUT = 15 * 60 # in seconds
|
||||
|
||||
if __name__ == "__main__":
|
||||
VMS_DIR.mkdir(exist_ok=True)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
tar_name = sys.argv[1]
|
||||
if not tar_name.endswith(".tar.gz"):
|
||||
print("Image name should be .tar.gz file", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
parser = argparse.ArgumentParser(description="Start E2E VM")
|
||||
# This is redundant, but kept to keep consistency with update_vm.py
|
||||
parser.add_argument("--vm", help="VM tar.gz. name", required=True)
|
||||
parser.add_argument(
|
||||
"--vmcli", help="Path to VMCLI binary", default="/opt/bin/VMCLI"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
tar_path = VMS_DIR / tar_name
|
||||
extracted_path = pathlib.Path(str(tar_path)[:-len(".tar.gz")])
|
||||
if not args.vm.endswith(".tar.gz"):
|
||||
logging.fatal("Image name should be .tar.gz file")
|
||||
|
||||
with open(SA_KEY, "rb") as key_file:
|
||||
storage_client = storage.Client(
|
||||
project=PROJECT,
|
||||
credentials=service_account.Credentials.from_service_account_info(
|
||||
json.load(key_file)),
|
||||
)
|
||||
bucket = storage_client.bucket(BUCKET)
|
||||
blob = bucket.get_blob(tar_name)
|
||||
|
||||
if blob is None:
|
||||
print("Specified image doesn't exist in GCS", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
local_ctime = os.stat(extracted_path).st_ctime
|
||||
except FileNotFoundError:
|
||||
local_ctime = 0
|
||||
|
||||
if blob.updated > datetime.datetime.fromtimestamp(
|
||||
local_ctime, tz=datetime.timezone.utc):
|
||||
print(f"VM {extracted_path} not present or not up to date, downloading...")
|
||||
|
||||
# Remove the old version of the image if present
|
||||
try:
|
||||
shutil.rmtree(extracted_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
blob.download_to_filename(tar_path)
|
||||
|
||||
hash_blob = bucket.get_blob(str(tar_name) + ".sha256")
|
||||
if hash_blob is None:
|
||||
print("Image hash doesn't exist in GCS", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sig_blob = bucket.get_blob(str(tar_name) + ".sha256.sig")
|
||||
if sig_blob is None:
|
||||
print("Image signature doesn't exist in GCS", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
hash_path = str(tar_path) + ".sha256"
|
||||
hash_blob.download_to_filename(hash_path)
|
||||
sig_path = str(tar_path) + ".sha256.sig"
|
||||
sig_blob.download_to_filename(sig_path)
|
||||
|
||||
# cosign OOMs trying to sign/verify the tarball itself, so sign/verify
|
||||
# the SHA256 of the tarball.
|
||||
print("Verifying signature...")
|
||||
|
||||
# Verify the signature of the hash file is OK
|
||||
subprocess.check_output([
|
||||
COSIGN,
|
||||
"verify-blob",
|
||||
"--key", PUBKEY,
|
||||
"--signature", sig_path,
|
||||
hash_path,
|
||||
])
|
||||
# Then verify that the hash matches what we downloaded
|
||||
subprocess.check_output(
|
||||
["shasum", "-a", "256", "-c", hash_path],
|
||||
cwd=VMS_DIR,
|
||||
)
|
||||
|
||||
print("Extracting...")
|
||||
subprocess.check_output(
|
||||
["tar", "-C", VMS_DIR, "-x", "-S", "-z", "-f", tar_path]
|
||||
)
|
||||
tar_path.unlink()
|
||||
tar_path = VMS_DIR / args.vm
|
||||
extracted_path = pathlib.Path(str(tar_path)[: -len(".tar.gz")])
|
||||
|
||||
with tempfile.TemporaryDirectory() as snapshot_dir:
|
||||
print(f"Snapshot: {snapshot_dir}")
|
||||
logging.info(f"Snapshot: {snapshot_dir}")
|
||||
# COW copy the image to this tempdir
|
||||
subprocess.check_output(["cp", "-rc", extracted_path, snapshot_dir])
|
||||
|
||||
# Get a JIT runner key
|
||||
github_token = os.environ["RUNNER_REG_TOKEN"]
|
||||
body = json.dumps(
|
||||
{
|
||||
"name": os.environ["GITHUB_RUN_ID"] + " inner",
|
||||
"runner_group_id": 1,
|
||||
"labels": [
|
||||
"self-hosted",
|
||||
"macOS",
|
||||
"ARM64",
|
||||
"e2e-vm",
|
||||
],
|
||||
"work_folder": "/tmp/_work",
|
||||
}
|
||||
)
|
||||
owner, repo = os.environ["GITHUB_REPOSITORY"].split("/", 1)
|
||||
request = urllib.request.Request(
|
||||
f"https://api.github.com/repos/{owner}/{repo}/actions/runners/generate-jitconfig",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/vnd.github+json",
|
||||
"Authorization": f"Bearer {github_token}",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
data=body.encode("utf-8"),
|
||||
)
|
||||
with urllib.request.urlopen(request) as response:
|
||||
jit_config = json.loads(response.read())["encoded_jit_config"]
|
||||
|
||||
logging.info("Got JIT runner config")
|
||||
|
||||
# Create a disk image to inject startup script
|
||||
init_dmg = pathlib.Path(snapshot_dir) / "init.dmg"
|
||||
subprocess.check_output(
|
||||
[
|
||||
"hdiutil",
|
||||
"create",
|
||||
"-attach",
|
||||
"-size",
|
||||
"1G",
|
||||
"-fs",
|
||||
"APFS",
|
||||
"-volname",
|
||||
"init",
|
||||
init_dmg,
|
||||
]
|
||||
)
|
||||
init_dmg_mount = pathlib.Path("/Volumes/init/")
|
||||
|
||||
# And populate startup script with runner and JIT key
|
||||
with open(init_dmg_mount / "run.sh", "w") as run_sh:
|
||||
run_sh.write(
|
||||
f"""#!/bin/sh
|
||||
set -xeuo pipefail
|
||||
|
||||
curl -L -o /tmp/runner.tar.gz 'https://github.com/actions/runner/releases/download/v2.316.0/actions-runner-osx-arm64-2.316.0.tar.gz'
|
||||
echo "8442d39e3d91b67807703ec0825cec4384837b583305ea43a495a9867b7222ca /tmp/runner.tar.gz" | shasum -a 256 -c -
|
||||
mkdir /tmp/runner
|
||||
cd /tmp/runner
|
||||
tar -xzf /tmp/runner.tar.gz
|
||||
./run.sh --jitconfig '{jit_config}'
|
||||
"""
|
||||
)
|
||||
os.chmod(init_dmg_mount / "run.sh", 0o755)
|
||||
subprocess.check_output(["hdiutil", "detach", init_dmg_mount])
|
||||
|
||||
logging.info("Created init.dmg")
|
||||
|
||||
# Create a disk image for USB testing
|
||||
usb_dmg = pathlib.Path(snapshot_dir) / "usb.dmg"
|
||||
subprocess.check_output(["hdiutil", "create", "-size", "100M",
|
||||
"-fs", "ExFAT", "-volname", "USB", usb_dmg])
|
||||
subprocess.check_output(
|
||||
[
|
||||
"hdiutil",
|
||||
"create",
|
||||
"-size",
|
||||
"100M",
|
||||
"-fs",
|
||||
"ExFAT",
|
||||
"-volname",
|
||||
"USB",
|
||||
usb_dmg,
|
||||
]
|
||||
)
|
||||
|
||||
logging.info("Created usb.dmg")
|
||||
|
||||
try:
|
||||
logging.info("Starting VM")
|
||||
subprocess.check_output(
|
||||
[VMCLI, pathlib.Path(snapshot_dir) / extracted_path.name, usb_dmg],
|
||||
[
|
||||
args.vmcli,
|
||||
pathlib.Path(snapshot_dir) / extracted_path.name,
|
||||
init_dmg,
|
||||
usb_dmg,
|
||||
],
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("VM timed out")
|
||||
|
||||
print("VM deleted")
|
||||
logging.warning("VM timed out")
|
||||
|
||||
logging.info("VM deleted")
|
||||
|
||||
105
Testing/integration/actions/update_vm.py
Normal file
105
Testing/integration/actions/update_vm.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download/update the given Santa E2E testing VM image."""
|
||||
import argparse
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from google.cloud import storage
|
||||
|
||||
PROJECT = "santa-e2e"
|
||||
BUCKET = "santa-e2e-vms"
|
||||
COSIGN = "/opt/bin/cosign"
|
||||
PUBKEY = "/opt/santa-e2e-vm-signer.pub"
|
||||
VMS_DIR = pathlib.Path.home() / "VMs"
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
parser = argparse.ArgumentParser(description="Start E2E VM")
|
||||
parser.add_argument("--vm", help="VM tar.gz. name", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
VMS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
tar_name = args.vm
|
||||
if not tar_name.endswith(".tar.gz"):
|
||||
logging.fatal("Image name should be .tar.gz file")
|
||||
|
||||
tar_path = VMS_DIR / tar_name
|
||||
extracted_path = pathlib.Path(str(tar_path)[: -len(".tar.gz")])
|
||||
|
||||
if "GOOGLE_APPLICATION_CREDENTIALS" not in os.environ:
|
||||
logging.fatal("Missing GCS credentials file")
|
||||
|
||||
storage_client = storage.Client(project=PROJECT)
|
||||
bucket = storage_client.bucket(BUCKET)
|
||||
blob = bucket.get_blob(tar_name)
|
||||
|
||||
if blob is None:
|
||||
logging.fatal("Specified image doesn't exist in GCS")
|
||||
|
||||
try:
|
||||
local_ctime = os.stat(extracted_path).st_ctime
|
||||
except FileNotFoundError:
|
||||
local_ctime = 0
|
||||
|
||||
if blob.updated > datetime.datetime.fromtimestamp(
|
||||
local_ctime, tz=datetime.timezone.utc
|
||||
):
|
||||
logging.info(
|
||||
f"VM {extracted_path} not present or not up to date, downloading..."
|
||||
)
|
||||
|
||||
# Remove the old version of the image if present
|
||||
try:
|
||||
shutil.rmtree(extracted_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
blob.download_to_filename(tar_path)
|
||||
|
||||
hash_blob = bucket.get_blob(str(tar_name) + ".sha256")
|
||||
if hash_blob is None:
|
||||
logging.fatal("Image hash doesn't exist in GCS")
|
||||
|
||||
sig_blob = bucket.get_blob(str(tar_name) + ".sha256.sig")
|
||||
if sig_blob is None:
|
||||
logging.fatal("Image signature doesn't exist in GCS")
|
||||
|
||||
hash_path = str(tar_path) + ".sha256"
|
||||
hash_blob.download_to_filename(hash_path)
|
||||
sig_path = str(tar_path) + ".sha256.sig"
|
||||
sig_blob.download_to_filename(sig_path)
|
||||
|
||||
# cosign OOMs trying to sign/verify the tarball itself, so sign/verify
|
||||
# the SHA256 of the tarball.
|
||||
logging.info("Verifying signature...")
|
||||
|
||||
# Verify the signature of the hash file is OK
|
||||
subprocess.check_output(
|
||||
[
|
||||
COSIGN,
|
||||
"verify-blob",
|
||||
"--key",
|
||||
PUBKEY,
|
||||
"--signature",
|
||||
sig_path,
|
||||
hash_path,
|
||||
]
|
||||
)
|
||||
# Then verify that the hash matches what we downloaded
|
||||
subprocess.check_output(
|
||||
["shasum", "-a", "256", "-c", hash_path],
|
||||
cwd=VMS_DIR,
|
||||
)
|
||||
|
||||
logging.info("Extracting...")
|
||||
subprocess.check_output(
|
||||
["tar", "-C", VMS_DIR, "-x", "-S", "-z", "-f", tar_path]
|
||||
)
|
||||
tar_path.unlink()
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -xo pipefail
|
||||
set -exo pipefail
|
||||
|
||||
GIT_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
find $GIT_ROOT \( -name "*.m" -o -name "*.h" -o -name "*.mm" -o -name "*.cc" \) -exec clang-format --Werror --dry-run {} \+
|
||||
find ${GIT_ROOT} \( -name "*.m" -o -name "*.h" -o -name "*.mm" -o -name "*.cc" \) -exec clang-format --Werror --dry-run {} \+
|
||||
|
||||
! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'
|
||||
|
||||
go install github.com/bazelbuild/buildtools/buildifier@latest
|
||||
~/go/bin/buildifier --lint=warn -r $GIT_ROOT
|
||||
~/go/bin/buildifier --lint=warn -r ${GIT_ROOT}
|
||||
|
||||
python3 -m pip install -q pylint
|
||||
find $GIT_ROOT \( -name "*.py" \) -exec python3 -m pylint --score n --rcfile=$GIT_ROOT/.pylintrc {} \+
|
||||
python3 -m pip install -q pyink
|
||||
python3 -m pyink --config ${GIT_ROOT}/.pyink-config --check ${GIT_ROOT}
|
||||
|
||||
183
WORKSPACE
183
WORKSPACE
@@ -3,166 +3,9 @@ workspace(name = "santa")
|
||||
load(
|
||||
"@bazel_tools//tools/build_defs/repo:git.bzl",
|
||||
"git_repository",
|
||||
"new_git_repository",
|
||||
)
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
# Abseil LTS branch, Aug 2023
|
||||
http_archive(
|
||||
name = "com_google_absl",
|
||||
sha256 = "59d2976af9d6ecf001a81a35749a6e551a335b949d34918cfade07737b9d93c5",
|
||||
strip_prefix = "abseil-cpp-20230802.0",
|
||||
urls = ["https://github.com/abseil/abseil-cpp/archive/refs/tags/20230802.0.tar.gz"],
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "com_google_protobuf",
|
||||
patch_args = ["-p1"],
|
||||
patches = ["//external_patches/com_google_protobuf:13636.patch"],
|
||||
sha256 = "07d69502e58248927b58c7d7e7424135272ba5b2852a753ab6b67e62d2d29355",
|
||||
strip_prefix = "protobuf-24.3",
|
||||
urls = ["https://github.com/protocolbuffers/protobuf/archive/v24.3.tar.gz"],
|
||||
)
|
||||
|
||||
# We don't directly use rules_python but several dependencies do and they disagree
|
||||
# about which version to use, so we force the latest.
|
||||
http_archive(
|
||||
name = "rules_python",
|
||||
sha256 = "48a838a6e1983e4884b26812b2c748a35ad284fd339eb8e2a6f3adf95307fbcd",
|
||||
strip_prefix = "rules_python-0.16.2",
|
||||
url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.16.2.tar.gz",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_apple",
|
||||
sha256 = "8ac4c7997d863f3c4347ba996e831b5ec8f7af885ee8d4fe36f1c3c8f0092b2c",
|
||||
url = "https://github.com/bazelbuild/rules_apple/releases/download/2.5.0/rules_apple.2.5.0.tar.gz",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_apple//apple:repositories.bzl", "apple_rules_dependencies")
|
||||
|
||||
apple_rules_dependencies()
|
||||
|
||||
load("@build_bazel_rules_swift//swift:repositories.bzl", "swift_rules_dependencies")
|
||||
|
||||
swift_rules_dependencies()
|
||||
|
||||
load(
|
||||
"@build_bazel_rules_swift//swift:extras.bzl",
|
||||
"swift_rules_extra_dependencies",
|
||||
)
|
||||
|
||||
swift_rules_extra_dependencies()
|
||||
|
||||
load("@build_bazel_apple_support//lib:repositories.bzl", "apple_support_dependencies")
|
||||
|
||||
apple_support_dependencies()
|
||||
|
||||
# Hedron Bazel Compile Commands Extractor
|
||||
# Allows integrating with clangd
|
||||
# https://github.com/hedronvision/bazel-compile-commands-extractor
|
||||
git_repository(
|
||||
name = "hedron_compile_commands",
|
||||
commit = "ac6411f8f347e5525038cb7858db4969db9e74f2",
|
||||
remote = "https://github.com/hedronvision/bazel-compile-commands-extractor.git",
|
||||
shallow_since = "1696885905 +0000",
|
||||
)
|
||||
|
||||
load("@hedron_compile_commands//:workspace_setup.bzl", "hedron_compile_commands_setup")
|
||||
|
||||
hedron_compile_commands_setup()
|
||||
|
||||
# Googletest - tag: release-1.12.1
|
||||
http_archive(
|
||||
name = "com_google_googletest",
|
||||
sha256 = "ab78fa3f912d44d38b785ec011a25f26512aaedc5291f51f3807c592b506d33a",
|
||||
strip_prefix = "googletest-58d77fa8070e8cec2dc1ed015d66b454c8d78850",
|
||||
urls = ["https://github.com/google/googletest/archive/58d77fa8070e8cec2dc1ed015d66b454c8d78850.zip"],
|
||||
)
|
||||
|
||||
# Note: Protobuf deps must be loaded after defining the ABSL archive since
|
||||
# protobuf repo would pull an in earlier version of ABSL.
|
||||
load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
|
||||
|
||||
protobuf_deps()
|
||||
|
||||
# Macops MOL* dependencies
|
||||
|
||||
git_repository(
|
||||
name = "MOLAuthenticatingURLSession",
|
||||
commit = "38b5ee46edb262481b16f950266a11d8cb77127c", # tag = v3.1
|
||||
remote = "https://github.com/google/macops-molauthenticatingurlsession.git",
|
||||
shallow_since = "1671479898 -0500",
|
||||
)
|
||||
|
||||
git_repository(
|
||||
name = "MOLCertificate",
|
||||
commit = "288553b8ac75d7dd68159ef5b57652a506b8217c", # tag = "v2.1",
|
||||
remote = "https://github.com/google/macops-molcertificate.git",
|
||||
shallow_since = "1561303966 -0400",
|
||||
)
|
||||
|
||||
git_repository(
|
||||
name = "MOLCodesignChecker",
|
||||
commit = "7ef66f1df15997defd7651b0ea5d6d9ec65a5b4f", # tag = "v2.2",
|
||||
remote = "https://github.com/google/macops-molcodesignchecker.git",
|
||||
shallow_since = "1561303990 -0400",
|
||||
)
|
||||
|
||||
git_repository(
|
||||
name = "MOLXPCConnection",
|
||||
commit = "2c67c925c2b57fea9af551295d2b6711b38bb224", # tag = v2.1
|
||||
remote = "https://github.com/google/macops-molxpcconnection.git",
|
||||
shallow_since = "1564684202 -0400",
|
||||
)
|
||||
|
||||
# FMDB
|
||||
|
||||
new_git_repository(
|
||||
name = "FMDB",
|
||||
build_file_content = """
|
||||
objc_library(
|
||||
name = "FMDB",
|
||||
srcs = glob(["src/fmdb/*.m"], exclude=["src/fmdb.m"]),
|
||||
hdrs = glob(["src/fmdb/*.h"]),
|
||||
includes = ["src"],
|
||||
sdk_dylibs = ["sqlite3"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
""",
|
||||
commit = "61e51fde7f7aab6554f30ab061cc588b28a97d04", # tag = 2.7.7
|
||||
remote = "https://github.com/ccgus/fmdb.git",
|
||||
shallow_since = "1589301502 -0700",
|
||||
)
|
||||
|
||||
# OCMock
|
||||
|
||||
new_git_repository(
|
||||
name = "OCMock",
|
||||
build_file_content = """
|
||||
objc_library(
|
||||
name = "OCMock",
|
||||
testonly = 1,
|
||||
hdrs = glob(["Source/OCMock/*.h"]),
|
||||
copts = [
|
||||
"-Wno-vla",
|
||||
],
|
||||
includes = [
|
||||
"Source",
|
||||
"Source/OCMock",
|
||||
],
|
||||
non_arc_srcs = glob(["Source/OCMock/*.m"]),
|
||||
pch = "Source/OCMock/OCMock-Prefix.pch",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
""",
|
||||
commit = "afd2c6924e8a36cb872bc475248b978f743c6050", # tag = v3.9.1
|
||||
patch_args = ["-p1"],
|
||||
patches = ["//external_patches/OCMock:503.patch"],
|
||||
remote = "https://github.com/erikdoe/ocmock",
|
||||
shallow_since = "1635703064 +0100",
|
||||
)
|
||||
|
||||
# Moroz (for testing)
|
||||
|
||||
http_archive(
|
||||
@@ -198,29 +41,3 @@ go_rules_dependencies()
|
||||
go_register_toolchains(version = "1.19.3")
|
||||
|
||||
gazelle_dependencies()
|
||||
|
||||
# Fuzzing
|
||||
|
||||
# rules_fuzzing requires an older python for now
|
||||
http_archive(
|
||||
name = "rules_python_fuzz",
|
||||
sha256 = "c03246c11efd49266e8e41e12931090b613e12a59e6f55ba2efd29a7cb8b4258",
|
||||
strip_prefix = "rules_python-0.11.0",
|
||||
url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.11.0.tar.gz",
|
||||
)
|
||||
|
||||
git_repository(
|
||||
name = "rules_fuzzing",
|
||||
commit = "b193df79b10dbfb4c623bda23e825e835f12bada", # Commit post PR 213 which fixes macOS
|
||||
remote = "https://github.com/bazelbuild/rules_fuzzing",
|
||||
repo_mapping = {"@rules_python": "@rules_python_fuzz"},
|
||||
shallow_since = "1668184479 -0500",
|
||||
)
|
||||
|
||||
load("@rules_fuzzing//fuzzing:repositories.bzl", "rules_fuzzing_dependencies")
|
||||
|
||||
rules_fuzzing_dependencies()
|
||||
|
||||
load("@rules_fuzzing//fuzzing:init.bzl", "rules_fuzzing_init")
|
||||
|
||||
rules_fuzzing_init()
|
||||
|
||||
@@ -5,7 +5,7 @@ nav_order: 1
|
||||
|
||||
# Welcome to the Santa documentation
|
||||
|
||||
Santa is a binary and file access authorization system for macOS. It consists of a system extension that allows or denies attempted executions using a set of rules stored in a local database, a GUI agent that notifies the user in case of a block decision, a sync daemon responsible for syncing the database and a server, and a command-line utility for managing the system.
|
||||
Santa is a binary and file access authorization system for macOS. It consists of a system extension that allows or denies attempted executions using a set of rules stored in a local database, a GUI agent that notifies the user in case of a block decision, a sync daemon responsible for syncing the database, and a server, and a command-line utility for managing the system.
|
||||
|
||||
It is named Santa because it keeps track of binaries that are naughty or nice.
|
||||
|
||||
@@ -13,19 +13,19 @@ The project and the latest release is available on [**GitHub**](https://github.c
|
||||
|
||||
## Features
|
||||
|
||||
* [**Multiple modes:**](concepts/mode.md) In the default `MONITOR` mode, all binaries except those marked as blocked will be allowed to run, whilst being logged and recorded in the events database. In `LOCKDOWN` mode, only listed binaries are allowed to run.
|
||||
* [**Multiple modes:**](concepts/mode.md) In the default `MONITOR` mode, all binaries except those marked as blocked will be allowed to run, while logged and recorded in the events database. In `LOCKDOWN` mode, only listed binaries are allowed to run.
|
||||
* [**Event logging:**](concepts/events.md) All binary launches are logged. When in either mode, all unknown or denied binaries are stored in the database to enable later aggregation.
|
||||
* [**Several supported rule types:**](concepts/rules.md) Executions can be allowed or denied by specifying rules based on several attributes. The supported rule types, in order of highest to lowest precedence are: CDHash, binary hash, Signing ID, certificate hash, or Team ID. Since multiple rules can apply to a given binary, Santa will apply the rule with the highest precedence (i.e. you could use a Team ID rule to allow all binaries from some organization, but also add a Signing ID rule to deny a specific binary). Rules based on code signature properties (Signing ID, certificate hash, and Team ID) only apply if a bianry's signature validates correctly.
|
||||
* [**Several supported rule types:**](concepts/rules.md) Executions can be allowed or denied by specifying rules based on several attributes. The supported rule types, in order of highest to lowest precedence are: CDHash, binary hash, Signing ID, certificate hash, or Team ID. Since multiple rules can apply to a given binary, Santa will apply the rule with the highest precedence (i.e. you could use a Team ID rule to allow all binaries from some organization, but also add a Signing ID rule to deny a specific binary). Rules based on code signature properties (Signing ID, certificate hash, and Team ID) only apply if a binary's signature validates correctly.
|
||||
* **Path-based rules (via NSRegularExpression/ICU):** Binaries can be allowed/blocked based on the path they are launched from by matching against a configurable regex.
|
||||
* [**Failsafe cert rules:**](concepts/rules.md#built-in-rules) You cannot put in a deny rule that would block the certificate used to sign launchd, a.k.a. pid 1, and therefore all components used in macOS. The binaries in every OS update (and in some cases entire new versions) are therefore automatically allowed. This does not affect binaries from Apple's App Store, which use various certs that change regularly for common apps. Likewise, you cannot block Santa itself.
|
||||
* [**Failsafe cert rules:**](concepts/rules.md#built-in-rules) You cannot put in a deny rule that would block the certificate used to sign launchd, a.k.a. pid 1, and therefore all components used in macOS. The binaries in every OS update (and in some cases entire new versions) are therefore automatically allowed. This does not affect binaries from Apple's App Store, which uses various certs that change regularly for common apps. Likewise, you cannot block Santa itself.
|
||||
* [**Components validate each other:**](binaries/index.md) Each of the components (the daemons, the GUI agent, and the command-line utility) communicate with each other using XPC and check that their signing certificates are identical before any communication is accepted.
|
||||
* **Caching:** Allowed binaries are cached so the processing required to make a request is only done if the binary isn't already cached.
|
||||
* **Caching:** Allowed binaries are cached, so the processing required to make a request is only done if the binary hasn't already been cached.
|
||||
|
||||
## Documentation overview
|
||||
|
||||
### Introduction
|
||||
|
||||
The following pages give an overview of how Santa accomplishes authorization at enterprise scale.
|
||||
The following pages provide an overview of how Santa accomplishes authorization at an enterprise scale.
|
||||
|
||||
* [Binary Authorization](introduction/binary-authorization-overview.md): How Santa makes allow or deny decisions for any execution taking place.
|
||||
* [Syncing](introduction/syncing-overview.md): How configuration and rules are applied from a sync server.
|
||||
|
||||
@@ -20,7 +20,7 @@ NOTE: Synchronization is now performed by its own agent, the `santasyncservice`.
|
||||
The phases of synchronization described below still apply, but references to how
|
||||
the process starts is outdated. This will be updated soon.
|
||||
|
||||
This is a high level overview of the syncing process. For a more a more detailed
|
||||
This is a high level overview of the syncing process. For a more detailed
|
||||
account of each part, see the respective documentation. The santaclt binary can
|
||||
be run in one of two modes, daemon and non-daemon. The non-daemon mode does one
|
||||
full sync and exits. This is the typical way a user will interact with Santa,
|
||||
@@ -29,8 +29,8 @@ syncs, listen for push notifications and upload events.
|
||||
|
||||
1. When the santad process starts up, it looks for a `SyncBaseURL` key/value in
|
||||
the config. If one exists it will `fork()` and `execve()` `santactl sync
|
||||
—-daemon`. Before the new process calls `execve()`, all privileges are
|
||||
dropped. All privileged actions are then restricted to the XPC interface
|
||||
—-daemon`. All privileges are dropped before the new process calls `execve()`.
|
||||
All privileged actions are then restricted to the XPC interface
|
||||
made available to santactl by santad. Since this santactl process is running
|
||||
as a daemon it too exports an XPC interface so santad can interact with the
|
||||
process efficiently and securely. To ensure syncing reliability santad will
|
||||
@@ -51,8 +51,8 @@ syncs, listen for push notifications and upload events.
|
||||
2. Firebase Cloud Messaging (FCM) can be used*. The sync server can send
|
||||
down a configuration in the preflight to have the santactl daemon to
|
||||
start listening for FCM messages. If a connection to FCM is made, the
|
||||
full sync interval drops to a default of 4 hours. This can be further
|
||||
configured by a preflight configuration. The FCM connection allows the
|
||||
full sync interval drops to a default of 4 hours. A preflight configuration can override this.
|
||||
The FCM connection allows the
|
||||
sync-sever to talk directly with Santa. This way we can reduce polling
|
||||
the sync server dramatically.
|
||||
5. Full syncs will continue to take place at their configured interval. If
|
||||
@@ -65,5 +65,5 @@ syncs, listen for push notifications and upload events.
|
||||
When running as a daemon, the santactl process makes available an XPC interface
|
||||
for use by santad. This allows santad to send blocked binary or bundle events
|
||||
directly to santactl for immediate upload to the sync-server, enabling a
|
||||
smoother user experience. The binary that was blocked on macOS is immediately
|
||||
smoother user experience. The binary blocked on macOS is immediately
|
||||
available for viewing or handling on the sync-server.
|
||||
|
||||
@@ -5,11 +5,10 @@ nav_order: 7
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Santa only blocks execution (execve and variants), it doesn't protect against dynamic libraries loaded with dlopen, libraries on disk that have been replaced, or libraries loaded using `DYLD_INSERT_LIBRARIES`.
|
||||
- Santa only blocks execution (execve and variants); it doesn't protect against dynamic libraries loaded with dlopen, libraries on disk that have been replaced, or libraries loaded using `DYLD_INSERT_LIBRARIES`.
|
||||
|
||||
- Scripts: Santa is currently written to ignore any execution that isn't a binary. After weighing the administration cost versus the benefit, we found it wasn't worthwhile to manage the execution of scripts. Additionally, a number of applications make use of temporary generated scripts and blocking these could cause problems. We're happy to revisit this (or at least make it an option) if it would be useful to others.
|
||||
- Scripts: Santa is written to ignore any execution that isn't a binary. After weighing the administrative cost versus the benefit, we found it wasn't worthwhile to manage the execution of scripts. Additionally, several applications make use of temporary scripts, and blocking these could cause problems. We're happy to revisit this (or at least make it an option) if it would be useful to others.
|
||||
|
||||
- USB Mass Storage Blocking: Santa's USB Mass Storage blocking feature is only meant to stop incidental
|
||||
- USB Mass Storage Blocking: Santa's USB Mass Storage blocking feature only stops incidental
|
||||
data exfiltration. It is not meant as a hard control. It cannot block:
|
||||
* Storage devices mounted during boot prior to Santa having an opportunity to begin authorizing mounts
|
||||
* Directly writing to an unmounted, but attached device
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
diff --git a/protobuf_deps.bzl b/protobuf_deps.bzl
|
||||
index ee552c13e..f0bba0385 100644
|
||||
--- a/protobuf_deps.bzl
|
||||
+++ b/protobuf_deps.bzl
|
||||
@@ -70,8 +70,8 @@ def protobuf_deps():
|
||||
_github_archive(
|
||||
name = "utf8_range",
|
||||
repo = "https://github.com/protocolbuffers/utf8_range",
|
||||
- commit = "de0b4a8ff9b5d4c98108bdfe723291a33c52c54f",
|
||||
- sha256 = "5da960e5e5d92394c809629a03af3c7709d2d3d0ca731dacb3a9fb4bf28f7702",
|
||||
+ commit = "d863bc33e15cba6d873c878dcca9e6fe52b2f8cb",
|
||||
+ sha256 = "568988b5f7261ca181468dba38849fabf59dd9200fb2ed4b2823da187ef84d8c",
|
||||
)
|
||||
|
||||
if not native.existing_rule("rules_cc"):
|
||||
@@ -1,5 +0,0 @@
|
||||
licenses(["notice"])
|
||||
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
)
|
||||
@@ -21,12 +21,12 @@ def santa_unit_test(
|
||||
name,
|
||||
srcs = [],
|
||||
deps = [],
|
||||
data = [],
|
||||
size = "medium",
|
||||
minimum_os_version = "11.0",
|
||||
resources = [],
|
||||
structured_resources = [],
|
||||
copts = [],
|
||||
data = [],
|
||||
**kwargs):
|
||||
apple_resource_group(
|
||||
name = "%s_resources" % name,
|
||||
@@ -40,7 +40,7 @@ def santa_unit_test(
|
||||
srcs = srcs,
|
||||
deps = deps,
|
||||
copts = copts,
|
||||
data = [":%s_resources" % name],
|
||||
data = data + [":%s_resources" % name],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -50,6 +50,5 @@ def santa_unit_test(
|
||||
minimum_os_version = minimum_os_version,
|
||||
deps = [":%s_lib" % name],
|
||||
size = size,
|
||||
data = data,
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
|
||||
51
non_module_deps.bzl
Normal file
51
non_module_deps.bzl
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Modules for dependencies not included in the Bazel Central Registry"""
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
||||
|
||||
def _non_module_deps_impl(_):
|
||||
# FMDB is used to access SQLite from Objective-C(++) code.
|
||||
git_repository(
|
||||
name = "FMDB",
|
||||
remote = "https://github.com/ccgus/fmdb.git",
|
||||
commit = "61e51fde7f7aab6554f30ab061cc588b28a97d04",
|
||||
shallow_since = "1589301502 -0700",
|
||||
build_file_content = """
|
||||
objc_library(
|
||||
name = "FMDB",
|
||||
srcs = glob(["src/fmdb/*.m"], exclude=["src/fmdb.m"]),
|
||||
hdrs = glob(["src/fmdb/*.h"]),
|
||||
includes = ["src"],
|
||||
sdk_dylibs = ["sqlite3"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
""",
|
||||
)
|
||||
|
||||
# OCMock is used in several tests.
|
||||
git_repository(
|
||||
name = "OCMock",
|
||||
build_file_content = """
|
||||
objc_library(
|
||||
name = "OCMock",
|
||||
testonly = 1,
|
||||
hdrs = glob(["Source/OCMock/*.h"]),
|
||||
copts = [
|
||||
"-Wno-vla",
|
||||
],
|
||||
includes = [
|
||||
"Source",
|
||||
"Source/OCMock",
|
||||
],
|
||||
non_arc_srcs = glob(["Source/OCMock/*.m"]),
|
||||
pch = "Source/OCMock/OCMock-Prefix.pch",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
""",
|
||||
commit = "afd2c6924e8a36cb872bc475248b978f743c6050", # tag = v3.9.1
|
||||
patch_args = ["-p1"],
|
||||
patches = ["//external_patches/OCMock:503.patch"],
|
||||
remote = "https://github.com/erikdoe/ocmock",
|
||||
shallow_since = "1635703064 +0100",
|
||||
)
|
||||
|
||||
non_module_deps = module_extension(implementation = _non_module_deps_impl)
|
||||
Reference in New Issue
Block a user