mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b824a8e3e0 | ||
|
|
25bf2a93e4 | ||
|
|
f1ea1b369f | ||
|
|
5503a88308 | ||
|
|
8cf0f8217d | ||
|
|
22799ffc2a | ||
|
|
cb61d0cc99 | ||
|
|
fb7447ceba | ||
|
|
45e51e9c09 | ||
|
|
b0f0cdd4e6 | ||
|
|
65090d3ef2 | ||
|
|
9c80f79d82 | ||
|
|
93adaea81e | ||
|
|
a125b340a5 | ||
|
|
fbd0de3d48 | ||
|
|
6f2ae62bce | ||
|
|
da29b20473 | ||
|
|
197109a8ee | ||
|
|
91f3168c7a | ||
|
|
a00ec41518 | ||
|
|
c32248aaf7 | ||
|
|
afd97bdf3e | ||
|
|
73c4875b1f | ||
|
|
916fc8c0e6 | ||
|
|
e59e6105f3 | ||
|
|
216ac811eb | ||
|
|
48f92f5913 | ||
|
|
6bb08d0490 | ||
|
|
82b71c0f20 | ||
|
|
10ccee9e4c | ||
|
|
acbbb9e7b0 | ||
|
|
3939ad9813 | ||
|
|
d20455252d | ||
|
|
5cd901034f | ||
|
|
4e82392370 | ||
|
|
19710f7233 | ||
|
|
27e32bd9ff | ||
|
|
c268ad4f9a | ||
|
|
f7a1a4cb39 | ||
|
|
ad6e03e6cc | ||
|
|
8ecc3f879a | ||
|
|
d51093501c | ||
|
|
05dd1b6215 | ||
|
|
8c3320e3e9 | ||
|
|
369dc9a63c | ||
|
|
7adc55007c | ||
|
|
fe6be921d3 | ||
|
|
23b31ec413 | ||
|
|
727b009a1c | ||
|
|
1c42f06135 | ||
|
|
e1cf8e70a3 | ||
|
|
7a500b8135 | ||
|
|
3702af0309 | ||
|
|
697cd29a0a | ||
|
|
5735a12424 | ||
|
|
07b8f2121d | ||
|
|
78a1a929fd | ||
|
|
9163417b54 | ||
|
|
fa6630a31a | ||
|
|
1f2b82fc58 | ||
|
|
b77b0142af | ||
|
|
2f80a42845 | ||
|
|
67db370492 | ||
|
|
a0319ecf52 | ||
|
|
16d0bd6db6 | ||
|
|
9e3943ec68 | ||
|
|
e461b4bfbc | ||
|
|
8f836afe86 | ||
|
|
04ad1c34ba | ||
|
|
c3042e21dc | ||
|
|
3ede20a121 | ||
|
|
976118cce4 | ||
|
|
ea85f0f539 | ||
|
|
d193b05057 | ||
|
|
9fb4f2e171 | ||
|
|
58cec5819a | ||
|
|
6ba5831f2d | ||
|
|
a22e3ead83 | ||
|
|
2611b551ce | ||
|
|
023f96f5c8 |
13
.github/workflows/check-markdown-links.yml
vendored
Normal file
13
.github/workflows/check-markdown-links.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Check Markdown links
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.md"
|
||||
|
||||
jobs:
|
||||
markdown-link-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
52
.github/workflows/ci.yml
vendored
52
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
preqs:
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
run_build_and_tests: ${{ steps.step1.outputs.run_build_and_tests }}
|
||||
build_driver: ${{ steps.step1.outputs.build_driver }}
|
||||
@@ -32,29 +32,42 @@ jobs:
|
||||
for file in `cat files.txt`; do
|
||||
if [[ $file = Source/* ]]; then
|
||||
build_and_run_test=1;
|
||||
if [[ $file = Source/santa_driver/* ]]; then
|
||||
if [[ $file = Source/santa_driver/* || $file = Source/common/* ]]; then
|
||||
build_driver=1;
|
||||
break;
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $build_and_run_test != 0 ]]; then
|
||||
if [[ $build_and_run_test != 0 ]]; then
|
||||
echo "NEED TO RUN BUILD AND TESTS"
|
||||
echo "::set-output name=run_build_and_tests::true"
|
||||
else
|
||||
else
|
||||
echo "::set-output name=run_build_and_tests::false"
|
||||
fi
|
||||
|
||||
|
||||
if [[ $build_driver != 0 ]]; then
|
||||
echo "NEED TO BUILD DRIVER"
|
||||
echo "::set-output name=build_driver::true"
|
||||
else
|
||||
else
|
||||
echo "::set-output name=build_driver::false"
|
||||
fi
|
||||
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [preqs]
|
||||
if: needs.preqs.outputs.run_build_and_tests == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run linters
|
||||
run: ./Testing/lint.sh
|
||||
|
||||
build_userspace:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-10.15, macos-11]
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [preqs]
|
||||
if: needs.preqs.outputs.run_build_and_tests == 'true'
|
||||
steps:
|
||||
@@ -63,7 +76,11 @@ jobs:
|
||||
run: bazel build --apple_generate_dsym -c opt :release --define=SANTA_BUILD_TYPE=ci
|
||||
|
||||
build_driver:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-10.15, macos-11]
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [preqs]
|
||||
if: needs.preqs.outputs.build_driver == 'true'
|
||||
steps:
|
||||
@@ -72,7 +89,11 @@ jobs:
|
||||
run: bazel build --apple_generate_dsym -c opt :release_driver --define=SANTA_BUILD_TYPE=ci
|
||||
|
||||
unit_tests:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-10.15, macos-11]
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [preqs]
|
||||
if: needs.preqs.outputs.run_build_and_tests == 'true'
|
||||
steps:
|
||||
@@ -81,7 +102,7 @@ jobs:
|
||||
run: bazel test :unit_tests --define=SANTA_BUILD_TYPE=ci
|
||||
|
||||
test_coverage:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-11
|
||||
needs: [preqs]
|
||||
if: needs.preqs.outputs.run_build_and_tests == 'true'
|
||||
steps:
|
||||
@@ -94,3 +115,12 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path-to-lcov: ./CoverageData/info.lcov
|
||||
flag-name: Unit
|
||||
|
||||
benchmark:
|
||||
runs-on: macos-11
|
||||
needs: [preqs]
|
||||
if: needs.preqs.outputs.run_build_and_tests == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run All Tests
|
||||
run: ./Testing/benchmark.sh
|
||||
|
||||
13
.github/workflows/continuous.yml
vendored
Normal file
13
.github/workflows/continuous.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: continuous
|
||||
on:
|
||||
schedule:
|
||||
- cron: '* 10 * * *' # Every day at 10:00 UTC
|
||||
workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
|
||||
|
||||
jobs:
|
||||
preqs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checks for flaky tests
|
||||
run: bazel test --test_strategy=exclusive --test_output=errors --runs_per_test 50 -t- :unit_tests --define=SANTA_BUILD_TYPE=ci
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -14,3 +14,7 @@ tulsigen-*
|
||||
*.pem
|
||||
*.p12
|
||||
*.keychain
|
||||
*.swp
|
||||
compile_commands.json
|
||||
.cache/
|
||||
.vscode/*
|
||||
|
||||
39
BUILD
39
BUILD
@@ -40,7 +40,6 @@ package_group(
|
||||
packages = ["//..."],
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
# Loading/Unloading/Reloading
|
||||
################################################################################
|
||||
@@ -49,6 +48,7 @@ run_command(
|
||||
cmd = """
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.google.santad.plist 2>/dev/null
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.google.santa.bundleservice.plist 2>/dev/null
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.google.santa.metricservice.plist 2>/dev/null
|
||||
sudo kextunload -b com.google.santa-driver 2>/dev/null
|
||||
launchctl unload /Library/LaunchAgents/com.google.santa.plist 2>/dev/null
|
||||
""",
|
||||
@@ -59,6 +59,7 @@ run_command(
|
||||
cmd = """
|
||||
sudo launchctl load /Library/LaunchDaemons/com.google.santad.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.google.santa.bundleservice.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.google.santa.metricservice.plist
|
||||
launchctl load /Library/LaunchAgents/com.google.santa.plist
|
||||
""",
|
||||
)
|
||||
@@ -95,11 +96,13 @@ genrule(
|
||||
"Conf/install.sh",
|
||||
"Conf/uninstall.sh",
|
||||
"Conf/com.google.santa.bundleservice.plist",
|
||||
"Conf/com.google.santa.metricservice.plist",
|
||||
"Conf/com.google.santad.plist",
|
||||
"Conf/com.google.santa.plist",
|
||||
"Conf/com.google.santa.asl.conf",
|
||||
"Conf/com.google.santa.newsyslog.conf",
|
||||
"Conf/Package/Makefile",
|
||||
"Conf/Package/Distribution.xml",
|
||||
"Conf/Package/notarization_tool.sh",
|
||||
"Conf/Package/package_and_sign.sh",
|
||||
"Conf/Package/postinstall",
|
||||
"Conf/Package/preinstall",
|
||||
],
|
||||
@@ -120,9 +123,9 @@ genrule(
|
||||
|
||||
# Copy config files
|
||||
for SRC in $(SRCS); do
|
||||
if [[ "$$(dirname $${SRC})" == *"Conf" ]]; then
|
||||
if [[ "$$(dirname $${SRC})" == *"Conf"* ]]; then
|
||||
mkdir -p $(@D)/conf
|
||||
cp $${SRC} $(@D)/conf/
|
||||
cp -H $${SRC} $(@D)/conf/
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -141,6 +144,10 @@ genrule(
|
||||
mkdir -p $(@D)/dsym
|
||||
cp -LR $$(dirname $$(dirname $${SRC})) $(@D)/dsym/santabundleservice.dSYM
|
||||
;;
|
||||
*santametricservice.dSYM*Info.plist)
|
||||
mkdir -p $(@D)/dsym
|
||||
cp -LR $$(dirname $$(dirname $${SRC})) $(@D)/dsym/santametricservice.dSYM
|
||||
;;
|
||||
*Santa.app.dSYM*Info.plist)
|
||||
mkdir -p $(@D)/dsym
|
||||
cp -LR $$(dirname $$(dirname $${SRC})) $(@D)/dsym/Santa.app.dSYM
|
||||
@@ -223,15 +230,17 @@ genrule(
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
"//Source/common:SantaCacheTest",
|
||||
"//Source/common:SNTFileInfoTest",
|
||||
"//Source/common:SNTPrefixTreeTest",
|
||||
"//Source/santactl:SNTCommandFileInfoTest",
|
||||
"//Source/santactl:SNTCommandSyncTest",
|
||||
"//Source/santad:SNTApplicationTest",
|
||||
"//Source/santad:SNTEventTableTest",
|
||||
"//Source/santad:SNTExecutionControllerTest",
|
||||
"//Source/santad:SNTRuleTableTest",
|
||||
"//Source/santad:SNTEndpointSecurityManagerTest",
|
||||
"//Source/common:unit_tests",
|
||||
"//Source/santactl:unit_tests",
|
||||
"//Source/santad:unit_tests",
|
||||
"//Source/santametricservice:unit_tests",
|
||||
"//Source/santasyncservice:unit_tests",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "benchmarks",
|
||||
tests = [
|
||||
"//Source/santad:SNTApplicationBenchmark",
|
||||
],
|
||||
)
|
||||
|
||||
18
Conf/Package/Distribution.xml
Normal file
18
Conf/Package/Distribution.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<installer-gui-script minSpecVersion="1">
|
||||
<title>Santa</title>
|
||||
<options customize="never" allow-external-scripts="no"/>
|
||||
|
||||
<choices-outline>
|
||||
<line choice="default" />
|
||||
</choices-outline>
|
||||
|
||||
<choice id="default">
|
||||
<pkg-ref id="com.google.santa"/>
|
||||
<pkg-ref id="com.google.santa-driver"/>
|
||||
</choice>
|
||||
|
||||
<pkg-ref id="com.google.santa">app.pkg</pkg-ref>
|
||||
<pkg-ref id="com.google.santa-driver" active="system.compareVersions(my.target.systemVersion.ProductVersion, '10.15') < 0">kext.pkg</pkg-ref>
|
||||
|
||||
</installer-gui-script>
|
||||
@@ -1,95 +0,0 @@
|
||||
#
|
||||
# Package Makefile for Santa
|
||||
# Requires TheLuggage (github.com/unixorn/luggage) to be installed
|
||||
#
|
||||
# Will generate a package based on the latest release. You can replace
|
||||
# the PACKAGE_VERSION variable with a specific variable instead if you wish.
|
||||
#
|
||||
|
||||
LUGGAGE:=/usr/local/share/luggage/luggage.make
|
||||
include ${LUGGAGE}
|
||||
|
||||
TITLE:=santa
|
||||
REVERSE_DOMAIN:=com.google
|
||||
|
||||
# Get latest Release version using the GitHub API. Each release is bound to a
|
||||
# git tag, which should always be a semantic version number. The most recent
|
||||
# release is always first in the API result.
|
||||
PACKAGE_VERSION:=$(shell curl -fs https://api.github.com/repos/google/santa/releases |\
|
||||
python -c 'import json, sys; print json.load(sys.stdin)[0]["tag_name"]' 2>/dev/null)
|
||||
|
||||
# Get the download URL for the latest Release. Each release should have a
|
||||
# tarball named santa-$version.tar.bz2 containing all of the files associated
|
||||
# with that release. The tarball layout is:
|
||||
#
|
||||
# santa-$version.tar.bz2
|
||||
# +--santa-$version
|
||||
# |-- binaries
|
||||
# | |-- santa-driver.kext
|
||||
# | |-- Santa.app
|
||||
# |-- conf
|
||||
# | |-- install.sh
|
||||
# | |-- com.google.santad.plist
|
||||
# | |-- com.google.santagui.plist
|
||||
# | +-- com.google.santa.asl.conf
|
||||
# | +-- com.google.santa.newsyslog.conf
|
||||
# +--dsym
|
||||
# |-- santa-driver.kext.dSYM
|
||||
# |-- Santa.app.dSYM
|
||||
# |-- santad.dSYM
|
||||
# +-- santactl.dSYM
|
||||
PACKAGE_DOWNLOAD_URL:="https://github.com/google/santa/releases/download/${PACKAGE_VERSION}/santa-${PACKAGE_VERSION}.tar.bz2"
|
||||
|
||||
PAYLOAD:=pack-Library-Extensions-santa-driver.kext \
|
||||
pack-applications-Santa.app \
|
||||
pack-Library-LaunchDaemons-com.google.santad.plist \
|
||||
pack-Library-LaunchAgents-com.google.santagui.plist \
|
||||
pack-etc-asl-com.google.santa.asl.conf \
|
||||
pack-etc-newsyslog.d-com.google.santa.newsyslog.conf \
|
||||
pack-script-preinstall \
|
||||
pack-script-postinstall
|
||||
|
||||
santa-driver.kext: download
|
||||
Santa.app: download
|
||||
com.google.santad.plist: download
|
||||
com.google.santagui.plist: download
|
||||
com.google.santa.asl.conf: download
|
||||
com.google.santa.newsyslog.conf: download
|
||||
|
||||
download:
|
||||
$(if $(PACKAGE_VERSION),, $(error GitHub API returned unexpected result. Wait a while and try again))
|
||||
|
||||
@curl -fL ${PACKAGE_DOWNLOAD_URL} | tar xvj --strip=2
|
||||
@rm -rf *.dSYM
|
||||
|
||||
pack-etc-asl-com.google.santa.asl.conf: com.google.santa.asl.conf l_private_etc
|
||||
@sudo mkdir -p ${WORK_D}/private/etc/asl
|
||||
@sudo chown root:wheel ${WORK_D}/private/etc/asl
|
||||
@sudo chmod 755 ${WORK_D}/private/etc/asl
|
||||
@sudo install -m 644 -o root -g wheel com.google.santa.asl.conf ${WORK_D}/private/etc/asl
|
||||
|
||||
pack-etc-newsyslog.d-com.google.santa.newsyslog.conf: com.google.santa.newsyslog.conf l_private_etc
|
||||
@sudo mkdir -p ${WORK_D}/private/etc/newsyslog.d
|
||||
@sudo chown root:wheel ${WORK_D}/private/etc/newsyslog.d
|
||||
@sudo chmod 755 ${WORK_D}/private/etc/newsyslog.d
|
||||
@sudo install -m 644 -o root -g wheel com.google.santa.newsyslog.conf ${WORK_D}/private/etc/newsyslog.d
|
||||
|
||||
pack-Library-Extensions-santa-driver.kext: santa-driver.kext l_Library
|
||||
@sudo mkdir -p ${WORK_D}/Library/Extensions
|
||||
@sudo ${DITTO} --noqtn santa-driver.kext ${WORK_D}/Library/Extensions/santa-driver.kext
|
||||
@sudo chown -R root:wheel ${WORK_D}/Library/Extensions/santa-driver.kext
|
||||
@sudo chmod -R 755 ${WORK_D}/Library/Extensions/santa-driver.kext
|
||||
|
||||
clean: myclean
|
||||
|
||||
myclean:
|
||||
@rm -rf *.dSYM
|
||||
@rm -rf Santa.app
|
||||
@rm -rf santa-driver.kext
|
||||
@rm -f config.plist
|
||||
@rm -f com.google.santa.asl.conf
|
||||
@rm -f com.google.santa.newsyslog.conf
|
||||
@rm -f com.google.santad.plist
|
||||
@rm -f com.google.santagui.plist
|
||||
@rm -f install.sh
|
||||
@rm -f uninstall.sh
|
||||
6
Conf/Package/notarization_tool.sh
Normal file
6
Conf/Package/notarization_tool.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Example NOTARIZATION_TOOL wrapper.
|
||||
|
||||
/usr/bin/xcrun altool --notarize-app "${2}" --primary-bundle-id "${4}" \
|
||||
-u "${NOTARIZATION_USERNAME}" -p "${NOTARIZATION_PASSWORD}"
|
||||
201
Conf/Package/package_and_sign.sh
Executable file
201
Conf/Package/package_and_sign.sh
Executable file
@@ -0,0 +1,201 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script signs all of Santa's components, verifies the signatures,
|
||||
# notarizes all of the components, staples them, packages them up, signs the
|
||||
# package, notarizes the package, puts the package in a DMG and notarizes the
|
||||
# DMG. It also outputs a single release tarball.
|
||||
# All of the following environment variables are required.
|
||||
|
||||
# RELEASE_ROOT is a required environment variable that points to the root
|
||||
# of an extracted release tarball produced with the :release and :release_driver
|
||||
# rules in Santa's main BUILD file.
|
||||
[[ -n "${RELEASE_ROOT}" ]] || die "RELEASE_ROOT unset"
|
||||
|
||||
# SIGNING_IDENTITY, SIGNING_TEAMID and SIGNING_KEYCHAIN are required environment
|
||||
# variables specifying the identity and keychain to pass to the codesign tool
|
||||
# and the team ID to use for verification.
|
||||
[[ -n "${SIGNING_IDENTITY}" ]] || die "SIGNING_IDENTITY unset"
|
||||
[[ -n "${SIGNING_TEAMID}" ]] || die "SIGNING_TEAMID unset"
|
||||
[[ -n "${SIGNING_KEYCHAIN}" ]] || die "SIGNING_KEYCHAIN unset"
|
||||
|
||||
# INSTALLER_SIGNING_IDENTITY and INSTALLER_SIGNING_KEYCHAIN are required
|
||||
# environment variables specifying the identity and keychain to use when signing
|
||||
# the distribution package.
|
||||
[[ -n "${INSTALLER_SIGNING_IDENTITY}" ]] || die "INSTALLER_SIGNING_IDENTITY unset"
|
||||
[[ -n "${INSTALLER_SIGNING_KEYCHAIN}" ]] || die "INSTALLER_SIGNING_KEYCHAIN unset"
|
||||
|
||||
# NOTARIZATION_TOOL is a required environment variable pointing to a wrapper
|
||||
# tool around the tool to use for notarization. The tool must take 2 flags:
|
||||
# --file
|
||||
# - pointing at a zip file containing the artifact to notarize
|
||||
# --primary-bundle-id
|
||||
# - specifying the CFBundleID of the artifact being notarized
|
||||
[[ -n "${NOTARIZATION_TOOL}" ]] || die "NOTARIZATION_TOOL unset"
|
||||
|
||||
# ARTIFACTS_DIR is a required environment variable pointing at a directory to
|
||||
# place the output artifacts in.
|
||||
[[ -n "${ARTIFACTS_DIR}" ]] || die "ARTIFACTS_DIR unset"
|
||||
|
||||
################################################################################
|
||||
|
||||
function die {
|
||||
echo "${@}"
|
||||
exit 2
|
||||
}
|
||||
|
||||
readonly INPUT_APP="${RELEASE_ROOT}/binaries/Santa.app"
|
||||
readonly INPUT_KEXT="${RELEASE_ROOT}/binaries/santa-driver.kext"
|
||||
readonly INPUT_SYSX="${INPUT_APP}/Contents/Library/SystemExtensions/com.google.santa.daemon.systemextension"
|
||||
readonly INPUT_SANTACTL="${INPUT_APP}/Contents/MacOS/santactl"
|
||||
readonly INPUT_SANTABS="${INPUT_APP}/Contents/MacOS/santabundleservice"
|
||||
readonly INPUT_SANTAMS="${INPUT_APP}/Contents/MacOS/santametricservice"
|
||||
|
||||
readonly RELEASE_NAME="santa-$(/usr/bin/defaults read "${INPUT_APP}/Contents/Info.plist" CFBundleVersion)"
|
||||
|
||||
readonly SCRATCH=$(/usr/bin/mktemp -d "${TMPDIR}/santa-"XXXXXX)
|
||||
readonly APP_PKG_ROOT="${SCRATCH}/app_pkg_root"
|
||||
readonly KEXT_PKG_ROOT="${SCRATCH}/kext_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}" "${KEXT_PKG_ROOT}" "${APP_PKG_SCRIPTS}" "${ENTITLEMENTS}"
|
||||
|
||||
readonly DMG_PATH="${ARTIFACTS_DIR}/${RELEASE_NAME}.dmg"
|
||||
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_SYSX}" "${INPUT_APP}" "${INPUT_KEXT}"; 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 \
|
||||
--options library,kill,runtime "${ARTIFACT}"
|
||||
done
|
||||
|
||||
# Notarize all the bundles
|
||||
for ARTIFACT in "${INPUT_SYSX}" "${INPUT_APP}" "${INPUT_KEXT}"; do
|
||||
BN=$(/usr/bin/basename "${ARTIFACT}")
|
||||
|
||||
echo "zipping ${BN}"
|
||||
/usr/bin/zip -9r "${SCRATCH}/${BN}.zip" "${ARTIFACT}"
|
||||
|
||||
echo "notarizing ${BN}"
|
||||
PBID=$(/usr/bin/defaults read "${ARTIFACT}/Contents/Info.plist" CFBundleIdentifier)
|
||||
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${BN}.zip" --primary-bundle-id "${PBID}"
|
||||
done
|
||||
|
||||
# Staple the App and Kext.
|
||||
for ARTIFACT in "${INPUT_APP}" "${INPUT_KEXT}"; do
|
||||
BN=$(/usr/bin/basename "${ARTIFACT}")
|
||||
|
||||
echo "stapling ${BN}"
|
||||
/usr/bin/xcrun stapler staple "${ARTIFACT}"
|
||||
done
|
||||
|
||||
# Ensure _CodeSignature/CodeResources files have 0644 permissions so they can
|
||||
# be verified without using sudo.
|
||||
/usr/bin/find "${RELEASE_ROOT}/binaries" -type f -name CodeResources -exec chmod 0644 {} \;
|
||||
/usr/bin/find "${RELEASE_ROOT}/binaries" -type d -exec chmod 0755 {} \;
|
||||
/usr/bin/find "${RELEASE_ROOT}/conf" -type f -name "com.google.santa*" -exec chmod 0644 {} \;
|
||||
|
||||
echo "verifying signatures"
|
||||
/usr/bin/codesign -vv -R="certificate leaf[subject.OU] = ${SIGNING_TEAMID}" \
|
||||
"${RELEASE_ROOT}/binaries/"* || die "bad signature"
|
||||
|
||||
echo "creating fresh release tarball"
|
||||
/bin/mkdir -p "${RELEASE_ROOT}/${RELEASE_NAME}"
|
||||
/bin/cp -r "${RELEASE_ROOT}/binaries" "${RELEASE_ROOT}/${RELEASE_NAME}"
|
||||
/bin/cp -r "${RELEASE_ROOT}/conf" "${RELEASE_ROOT}/${RELEASE_NAME}"
|
||||
/bin/cp -r "${RELEASE_ROOT}/dsym" "${RELEASE_ROOT}/${RELEASE_NAME}"
|
||||
/usr/bin/tar -C "${RELEASE_ROOT}" -czvf "${TAR_PATH}" "${RELEASE_NAME}" || die "failed to create release tarball"
|
||||
|
||||
echo "creating app pkg"
|
||||
/bin/mkdir -p "${APP_PKG_ROOT}/Applications" \
|
||||
"${APP_PKG_ROOT}/Library/LaunchAgents" \
|
||||
"${APP_PKG_ROOT}/Library/LaunchDaemons" \
|
||||
"${APP_PKG_ROOT}/private/etc/asl" \
|
||||
"${APP_PKG_ROOT}/private/etc/newsyslog.d"
|
||||
/bin/cp -vXR "${RELEASE_ROOT}/binaries/Santa.app" "${APP_PKG_ROOT}/Applications/"
|
||||
/bin/cp -vX "${RELEASE_ROOT}/conf/com.google.santad.plist" "${APP_PKG_ROOT}/Library/LaunchDaemons/"
|
||||
/bin/cp -vX "${RELEASE_ROOT}/conf/com.google.santa.plist" "${APP_PKG_ROOT}/Library/LaunchAgents/"
|
||||
/bin/cp -vX "${RELEASE_ROOT}/conf/com.google.santa.bundleservice.plist" "${APP_PKG_ROOT}/Library/LaunchDaemons/"
|
||||
/bin/cp -vX "${RELEASE_ROOT}/conf/com.google.santa.metricservice.plist" "${APP_PKG_ROOT}/Library/LaunchDaemons/"
|
||||
/bin/cp -vX "${RELEASE_ROOT}/conf/com.google.santa.asl.conf" "${APP_PKG_ROOT}/private/etc/asl/"
|
||||
/bin/cp -vX "${RELEASE_ROOT}/conf/com.google.santa.newsyslog.conf" "${APP_PKG_ROOT}/private/etc/newsyslog.d/"
|
||||
/bin/cp -vXL "${SCRIPT_PATH}/preinstall" "${APP_PKG_SCRIPTS}/"
|
||||
/bin/cp -vXL "${SCRIPT_PATH}/postinstall" "${APP_PKG_SCRIPTS}/"
|
||||
/bin/chmod +x "${APP_PKG_SCRIPTS}/"*
|
||||
|
||||
# Disable bundle relocation.
|
||||
/usr/bin/pkgbuild --analyze --root "${APP_PKG_ROOT}" "${SCRATCH}/component.plist"
|
||||
/usr/bin/plutil -replace BundleIsRelocatable -bool NO "${SCRATCH}/component.plist"
|
||||
/usr/bin/plutil -replace BundleIsVersionChecked -bool NO "${SCRATCH}/component.plist"
|
||||
/usr/bin/plutil -replace BundleOverwriteAction -string upgrade "${SCRATCH}/component.plist"
|
||||
/usr/bin/plutil -replace ChildBundles -json "[]" "${SCRATCH}/component.plist"
|
||||
|
||||
# Build app package
|
||||
/usr/bin/pkgbuild --identifier "com.google.santa" \
|
||||
--version "$(echo "${RELEASE_NAME}" | cut -d - -f2)" \
|
||||
--root "${APP_PKG_ROOT}" \
|
||||
--component-plist "${SCRATCH}/component.plist" \
|
||||
--scripts "${APP_PKG_SCRIPTS}" \
|
||||
"${SCRATCH}/app.pkg"
|
||||
|
||||
echo "creating kext pkg"
|
||||
/bin/mkdir -p "${KEXT_PKG_ROOT}/Library/Extensions"
|
||||
/bin/cp -vXR "${RELEASE_ROOT}/binaries/santa-driver.kext" "${KEXT_PKG_ROOT}/Library/Extensions/"
|
||||
/usr/bin/pkgbuild --analyze --root "${KEXT_PKG_ROOT}" "${SCRATCH}/component.plist"
|
||||
/usr/bin/plutil -replace BundleIsRelocatable -bool NO "${SCRATCH}/component.plist"
|
||||
/usr/bin/plutil -replace BundleIsVersionChecked -bool NO "${SCRATCH}/component.plist"
|
||||
/usr/bin/plutil -replace BundleOverwriteAction -string upgrade "${SCRATCH}/component.plist"
|
||||
/usr/bin/plutil -replace ChildBundles -json "[]" "${SCRATCH}/component.plist"
|
||||
|
||||
# Build kext package
|
||||
/usr/bin/pkgbuild --identifier "com.google.santa-driver" \
|
||||
--version "$(echo "${RELEASE_NAME}" | cut -d - -f2)" \
|
||||
--root "${KEXT_PKG_ROOT}" \
|
||||
--component-plist "${SCRATCH}/component.plist" \
|
||||
"${SCRATCH}/kext.pkg"
|
||||
|
||||
# Build signed distribution package
|
||||
echo "productbuild pkg"
|
||||
/bin/mkdir -p "${SCRATCH}/${RELEASE_NAME}"
|
||||
/usr/bin/productbuild \
|
||||
--distribution "${SCRIPT_PATH}/Distribution.xml" \
|
||||
--package-path "${SCRATCH}" \
|
||||
--sign "${INSTALLER_SIGNING_IDENTITY}" --keychain "${INSTALLER_SIGNING_KEYCHAIN}" \
|
||||
"${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg"
|
||||
|
||||
echo "verifying pkg signature"
|
||||
/usr/sbin/pkgutil --check-signature "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" || die "bad pkg signature"
|
||||
|
||||
echo "notarizing pkg"
|
||||
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" \
|
||||
--primary-bundle-id "com.google.santa"
|
||||
|
||||
echo "stapling pkg"
|
||||
/usr/bin/xcrun stapler staple "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" || die "failed to staple pkg"
|
||||
|
||||
echo "wrapping pkg in dmg"
|
||||
/usr/bin/hdiutil create -fs HFS+ -format UDZO \
|
||||
-volname "${RELEASE_NAME}" \
|
||||
-ov -imagekey zlib-level=9 \
|
||||
-srcfolder "${SCRATCH}/${RELEASE_NAME}/" "${DMG_PATH}" || die "failed to wrap pkg in dmg"
|
||||
|
||||
echo "notarizing dmg"
|
||||
"${NOTARIZATION_TOOL}" --file "${DMG_PATH}" --primary-bundle-id "com.google.santa"
|
||||
|
||||
echo "stapling dmg"
|
||||
/usr/bin/xcrun stapler staple "${DMG_PATH}" || die "failed to staple dmg"
|
||||
@@ -24,6 +24,9 @@ mkdir -p /usr/local/bin
|
||||
# Load com.google.santa.bundleservice
|
||||
/bin/launchctl load -w /Library/LaunchDaemons/com.google.santa.bundleservice.plist
|
||||
|
||||
# Load com.google.santa.metricservice
|
||||
/bin/launchctl load -w /Library/LaunchDaemons/com.google.santa.metricservice.plist
|
||||
|
||||
GUI_USER=$(/usr/bin/stat -f '%u' /dev/console)
|
||||
[[ -z "${GUI_USER}" ]] && exit 0
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
/bin/launchctl remove com.google.santad || true
|
||||
/bin/launchctl remove com.google.santa.bundleservice || true
|
||||
/bin/launchctl remove com.google.santa.metricservice || true
|
||||
|
||||
/bin/sleep 1
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# Copy this file to /etc/asl to log all messages from santa-driver to the log file
|
||||
> /var/db/santa/santa.log format="[$((Time)(ISO8601Z.3))] $Message" mode=0644 rotate=seq compress file_max=25M all_max=100M uid=0 gid=0
|
||||
? [= Sender kernel] [S= Message santa-driver:] claim
|
||||
? [= Sender kernel] [S= Message santa-driver:] file /var/db/santa/santa.log
|
||||
? [= Facility com.google.santa] claim
|
||||
? [= Facility com.google.santa] file /var/db/santa/santa.log
|
||||
22
Conf/com.google.santa.metricservice.plist
Normal file
22
Conf/com.google.santa.metricservice.plist
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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.metricservice</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Applications/Santa.app/Contents/MacOS/santametricservice</string>
|
||||
<string>--syslog</string>
|
||||
</array>
|
||||
<key>MachServices</key>
|
||||
<dict>
|
||||
<key>com.google.santa.metricservice</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,2 +1,2 @@
|
||||
# logfilename [owner:group] mode count size(KiB) when flags [/pid_file] # [sig_num]
|
||||
/var/db/santa/santa.log root:wheel 644 10 25000 * NZ
|
||||
/var/db/santa/santa.log root:wheel 644 10 25000 * Z
|
||||
|
||||
@@ -24,6 +24,9 @@ fi
|
||||
# Unload bundle service
|
||||
/bin/launchctl remove com.google.santa.bundleservice >/dev/null 2>&1
|
||||
|
||||
# Unload metric service
|
||||
/bin/launchctl remove com.google.santa.metricservice >/dev/null 2>&1
|
||||
|
||||
# Unload kext.
|
||||
/sbin/kextunload -b com.google.santa-driver >/dev/null 2>&1
|
||||
|
||||
@@ -43,6 +46,7 @@ GUI_USER=$(/usr/bin/stat -f '%u' /dev/console)
|
||||
/bin/rm /usr/sbin/santactl >/dev/null 2>&1
|
||||
/bin/rm -rf /Applications/Santa.app 2>&1
|
||||
/bin/rm -rf /Library/Extensions/santa-driver.kext 2>&1
|
||||
/bin/rm /etc/asl/com.google.santa.asl.conf
|
||||
|
||||
# Copy new files.
|
||||
/bin/mkdir -p /var/db/santa
|
||||
@@ -58,8 +62,8 @@ GUI_USER=$(/usr/bin/stat -f '%u' /dev/console)
|
||||
|
||||
/bin/cp ${CONF}/com.google.santa.plist /Library/LaunchAgents
|
||||
/bin/cp ${CONF}/com.google.santa.bundleservice.plist /Library/LaunchDaemons
|
||||
/bin/cp ${CONF}/com.google.santa.metricservice.plist /Library/LaunchDaemons
|
||||
/bin/cp ${CONF}/com.google.santad.plist /Library/LaunchDaemons
|
||||
/bin/cp ${CONF}/com.google.santa.asl.conf /etc/asl/
|
||||
/bin/cp ${CONF}/com.google.santa.newsyslog.conf /etc/newsyslog.d/
|
||||
|
||||
# Reload syslogd to pick up ASL configuration change.
|
||||
@@ -71,6 +75,9 @@ GUI_USER=$(/usr/bin/stat -f '%u' /dev/console)
|
||||
# Load com.google.santa.bundleservice
|
||||
/bin/launchctl load /Library/LaunchDaemons/com.google.santa.bundleservice.plist
|
||||
|
||||
# Load com.google.santa.metricservice
|
||||
/bin/launchctl load /Library/LaunchDaemons/com.google.santa.metricservice.plist
|
||||
|
||||
# Load GUI agent if someone is logged in.
|
||||
[[ -z "${GUI_USER}" ]] && exit 0
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ user=$(/usr/bin/stat -f '%u' /dev/console)
|
||||
/bin/rm -f /Library/LaunchAgents/com.google.santa.plist
|
||||
/bin/rm -f /Library/LaunchDaemons/com.google.santad.plist
|
||||
/bin/rm -f /Library/LaunchDaemons/com.google.santa.bundleservice.plist
|
||||
/bin/rm -f /Library/LaunchDaemons/com.google.santa.metricservice.plist
|
||||
/bin/rm -f /private/etc/asl/com.google.santa.asl.conf
|
||||
/bin/rm -f /private/etc/newsyslog.d/com.google.santa.newsyslog.conf
|
||||
/bin/rm -f /usr/local/bin/santactl # just a symlink
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include <SNTCommandSyncConstants.h>
|
||||
#include <SNTCommandSyncRuleDownload.h>
|
||||
#include <SNTCommandSyncState.h>
|
||||
#include <SNTRule.h>
|
||||
#include <SNTSyncConstants.h>
|
||||
#include <SNTSyncRuleDownload.h>
|
||||
#include <SNTSyncState.h>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size) {
|
||||
NSData *buffer = [NSData dataWithBytes:static_cast<const void *>(data) length:size];
|
||||
@@ -41,12 +41,12 @@ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size
|
||||
return 0;
|
||||
}
|
||||
|
||||
SNTCommandSyncState *state = [[SNTCommandSyncState alloc] init];
|
||||
SNTSyncState *state = [[SNTSyncState alloc] init];
|
||||
if (!state) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
SNTCommandSyncRuleDownload *obj = [[SNTCommandSyncRuleDownload alloc] initWithState:state];
|
||||
SNTSyncRuleDownload *obj = [[SNTSyncRuleDownload alloc] initWithState:state];
|
||||
if (!obj) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size
|
||||
SNTRule *newRule = [[SNTRule alloc] init];
|
||||
newRule.state = (SNTRuleState)input_data.state;
|
||||
newRule.type = (SNTRuleType)input_data.type;
|
||||
newRule.shasum = @(input_data.hash);
|
||||
newRule.identifier = @(input_data.hash);
|
||||
newRule.customMsg = @"";
|
||||
|
||||
MOLXPCConnection *daemonConn = [SNTXPCControlInterface configuredConnection];
|
||||
@@ -62,9 +62,9 @@ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size
|
||||
reply:^(NSError *error) {
|
||||
if (!error) {
|
||||
if (newRule.state == SNTRuleStateRemove) {
|
||||
printf("Removed rule for SHA-256: %s.\n", [newRule.shasum UTF8String]);
|
||||
printf("Removed rule for SHA-256: %s.\n", [newRule.identifier UTF8String]);
|
||||
} else {
|
||||
printf("Added rule for SHA-256: %s.\n", [newRule.shasum UTF8String]);
|
||||
printf("Added rule for SHA-256: %s.\n", [newRule.identifier UTF8String]);
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
85
README.md
85
README.md
@@ -4,22 +4,20 @@
|
||||
<img src="./Source/santa/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
</p>
|
||||
|
||||
Santa is a binary authorization system for macOS. It consists of a kernel
|
||||
extension (or a system extension on macOS 10.15+) that monitors for executions,
|
||||
a userland daemon that makes execution decisions based on the contents of a
|
||||
SQLite database, a GUI agent that notifies the user in case of a block decision
|
||||
Santa is a binary authorization system for macOS. It consists of a system or
|
||||
kernel extension (depending on the macOS version) that monitors for executions,
|
||||
a daemon that makes execution decisions based on the contents of a local
|
||||
database, a GUI agent that notifies the user in case of a block decision
|
||||
and a command-line utility for managing the system and synchronizing the
|
||||
database with a server.
|
||||
|
||||
It is named Santa because it keeps track of binaries that are naughty or nice.
|
||||
|
||||
Santa is a project of Google's Macintosh Operations Team.
|
||||
|
||||
# Docs
|
||||
|
||||
The Santa docs are stored in the
|
||||
[Docs](https://github.com/google/santa/blob/master/docs) directory. The docs are
|
||||
published at http://santa.dev.
|
||||
[Docs](https://github.com/google/santa/blob/main/docs) directory and published
|
||||
at http://santa.dev.
|
||||
|
||||
The docs include deployment options, details on how parts of Santa work and
|
||||
instructions for developing Santa itself.
|
||||
@@ -31,14 +29,14 @@ the [santa-dev](https://groups.google.com/forum/#!forum/santa-dev) group is a
|
||||
great place.
|
||||
|
||||
If you believe you have a bug, feel free to report [an
|
||||
issue](https://github.com/google/santa/isues) and we'll respond as soon as we
|
||||
issue](https://github.com/google/santa/issues) and we'll respond as soon as we
|
||||
can.
|
||||
|
||||
If you believe you've found a vulnerability, please read the
|
||||
[security policy](https://github.com/google/santa/security/policy) for
|
||||
disclosure reporting.
|
||||
|
||||
# Admin-Related Features
|
||||
# Features
|
||||
|
||||
* Multiple modes: In the default MONITOR mode, all binaries except those marked
|
||||
as blocked will be allowed to run, whilst being logged and recorded in
|
||||
@@ -74,6 +72,14 @@ disclosure reporting.
|
||||
common apps. Likewise, you cannot block Santa itself, and Santa uses a
|
||||
distinct separate cert than other Google apps.
|
||||
|
||||
* Userland components validate each other: each of the userland components (the
|
||||
daemon, 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.
|
||||
|
||||
# Intentions and Expectations
|
||||
|
||||
No single system or process will stop *all* attacks, or provide 100% security.
|
||||
@@ -89,36 +95,11 @@ protect hosts in whatever other ways you see fit.
|
||||
|
||||
# Security and Performance-Related Features
|
||||
|
||||
* In-kernel caching: allowed binaries are cached in the kernel so the
|
||||
processing required to make a request is only done if the binary isn't
|
||||
already cached.
|
||||
|
||||
* Userland components validate each other: each of the userland components (the
|
||||
daemon, 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.
|
||||
|
||||
* Kext uses only KPIs: the kernel extension only uses provided kernel
|
||||
programming interfaces to do its job. This means that the kext code should
|
||||
continue to work across OS versions.
|
||||
|
||||
# Known Issues
|
||||
|
||||
* 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`. As of version
|
||||
0.9.1 we *do* address [__PAGEZERO missing issues](b87482e) that were
|
||||
exploited in some versions of macOS. We are working on also protecting
|
||||
against similar avenues of attack.
|
||||
|
||||
* Kext communication security: the kext will only accept a connection from a
|
||||
single client at a time and said client must be running as root. We haven't
|
||||
yet found a good way to ensure the kext only accepts connections from a valid
|
||||
client.
|
||||
|
||||
* Database protection: the SQLite database is installed with permissions so
|
||||
that only the root user can read/write it. We're considering approaches to
|
||||
secure this further.
|
||||
replaced, or libraries loaded using `DYLD_INSERT_LIBRARIES`.
|
||||
|
||||
* Scripts: Santa is currently written to ignore any execution that isn't a
|
||||
binary. This is because after weighing the administration cost vs the
|
||||
@@ -133,19 +114,17 @@ protect hosts in whatever other ways you see fit.
|
||||
management server, which uploads events that have occurred on the machine and
|
||||
downloads new rules. There are several open-source servers you can sync with:
|
||||
|
||||
* [Upvote](https://github.com/google/upvote) - An AppEngine-based server
|
||||
that implements social voting to make managing a large fleet easier.
|
||||
* [Moroz](https://github.com/groob/moroz) - A simple golang server that
|
||||
serves hardcoded rules from simple configuration files.
|
||||
* [Rudolph](https://github.com/airbnb/rudolph) - An AWS-based serverless sync service
|
||||
primarily built on API GW, DynamoDB, and Lambda components to reduce operational burden.
|
||||
Rudolph is designed to be fast, easy-to-use, and cost-efficient.
|
||||
* [Zentral](https://github.com/zentralopensource/zentral/wiki) - A
|
||||
centralized service that pulls data from multiple sources and deploy
|
||||
configurations to multiple services.
|
||||
* [Zercurity](https://github.com/zercurity/zercurity) - A dockerized service
|
||||
for managing and monitoring applications across a large fleet utilizing
|
||||
Santa + Osquery.
|
||||
* [Rudolph](https://github.com/airbnb/rudolph) - An AWS-based serverless sync service
|
||||
primarily built on API GW, DynamoDB, and Lambda components to reduce operational burden.
|
||||
Rudolph is designed to be fast, easy-to-use, and cost-efficient.
|
||||
|
||||
* Alternatively, `santactl` can configure rules locally (without a sync
|
||||
server).
|
||||
@@ -158,31 +137,9 @@ instead.
|
||||
|
||||
<p align="center"> <img src="https://thumbs.gfycat.com/MadFatalAmphiuma-small.gif" alt="Santa Block Video" /> </p>
|
||||
|
||||
# Kext Signing
|
||||
Kernel extensions on macOS 10.9 and later must be signed using an Apple-provided
|
||||
Developer ID certificate with a kernel extension flag. Without it, the only way
|
||||
to load an extension is to enable kext-dev-mode or disable SIP, depending on
|
||||
the OS version.
|
||||
|
||||
There are two possible solutions for this, for distribution purposes:
|
||||
|
||||
1) Use a [pre-built, pre-signed
|
||||
version](https://github.com/google/santa/releases) of the kext that we supply.
|
||||
Each time changes are made to the kext code we will update the pre-built
|
||||
version that you can make use of. This doesn't prevent you from making changes
|
||||
to the non-kext parts of Santa and distributing those. If you make changes to
|
||||
the kext and make a pull request, we can merge them in and distribute a new
|
||||
version of the pre-signed kext.
|
||||
|
||||
2) Apply for your own [kext signing
|
||||
certificate](https://developer.apple.com/contact/kext/). Apple will only grant
|
||||
this for broad distribution within an organization, they won't issue them just
|
||||
for testing purposes.
|
||||
|
||||
# Contributing
|
||||
Patches to this project are very much welcome. Please see the
|
||||
[CONTRIBUTING](https://github.com/google/santa/blob/master/CONTRIBUTING.md)
|
||||
file.
|
||||
[CONTRIBUTING](https://santa.dev/development/contributing) doc.
|
||||
|
||||
# Disclaimer
|
||||
This is **not** an official Google product.
|
||||
|
||||
@@ -87,6 +87,10 @@ objc_library(
|
||||
cc_library(
|
||||
name = "SNTKernelCommon",
|
||||
hdrs = ["SNTKernelCommon.h"],
|
||||
defines = [
|
||||
"TARGET_OS_OSX",
|
||||
"TARGET_OS_MAC",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
@@ -96,7 +100,11 @@ cc_library(
|
||||
"-mkernel",
|
||||
"-I__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks/Kernel.framework/Headers",
|
||||
],
|
||||
defines = ["KERNEL"],
|
||||
defines = [
|
||||
"KERNEL",
|
||||
"TARGET_OS_OSX",
|
||||
"TARGET_OS_MAC",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
@@ -123,7 +131,11 @@ cc_library(
|
||||
"-mkernel",
|
||||
"-I__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks/Kernel.framework/Headers",
|
||||
],
|
||||
defines = ["KERNEL"],
|
||||
defines = [
|
||||
"KERNEL",
|
||||
"TARGET_OS_OSX",
|
||||
"TARGET_OS_MAC",
|
||||
],
|
||||
deps = [":SNTLoggingKernel"],
|
||||
)
|
||||
|
||||
@@ -201,6 +213,7 @@ objc_library(
|
||||
name = "SNTMetricSet",
|
||||
srcs = ["SNTMetricSet.m"],
|
||||
hdrs = ["SNTMetricSet.h"],
|
||||
deps = [":SNTCommonEnums"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
@@ -242,9 +255,9 @@ santa_unit_test(
|
||||
name = "SNTFileInfoTest",
|
||||
srcs = ["SNTFileInfoTest.m"],
|
||||
resources = [
|
||||
"testdata/32bitplist",
|
||||
"testdata/bad_pagezero",
|
||||
"testdata/missing_pagezero",
|
||||
"testdata/32bitplist",
|
||||
],
|
||||
structured_resources = glob([
|
||||
"testdata/BundleExample.app/**",
|
||||
@@ -264,3 +277,14 @@ santa_unit_test(
|
||||
srcs = ["SNTMetricSetTest.m"],
|
||||
deps = [":SNTMetricSet"],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":SNTFileInfoTest",
|
||||
":SNTMetricSetTest",
|
||||
":SNTPrefixTreeTest",
|
||||
":SantaCacheTest",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
@property NSString *certSHA256;
|
||||
@property NSString *certCommonName;
|
||||
@property NSArray<MOLCertificate *> *certChain;
|
||||
@property NSString *teamID;
|
||||
|
||||
@property NSString *quarantineURL;
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ typedef NS_ENUM(NSInteger, SNTRuleType) {
|
||||
|
||||
SNTRuleTypeBinary = 1,
|
||||
SNTRuleTypeCertificate = 2,
|
||||
SNTRuleTypeTeamID = 3,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTRuleState) {
|
||||
@@ -55,6 +56,7 @@ typedef NS_ENUM(NSInteger, SNTEventState) {
|
||||
SNTEventStateBlockBinary = 1 << 17,
|
||||
SNTEventStateBlockCertificate = 1 << 18,
|
||||
SNTEventStateBlockScope = 1 << 19,
|
||||
SNTEventStateBlockTeamID = 1 << 20,
|
||||
|
||||
// Bits 24-31 store allow decision types
|
||||
SNTEventStateAllowUnknown = 1 << 24,
|
||||
@@ -64,6 +66,7 @@ typedef NS_ENUM(NSInteger, SNTEventState) {
|
||||
SNTEventStateAllowCompiler = 1 << 28,
|
||||
SNTEventStateAllowTransitive = 1 << 29,
|
||||
SNTEventStateAllowPendingTransitive = 1 << 30,
|
||||
SNTEventStateAllowTeamID = 1 << 31,
|
||||
|
||||
// Block and Allow masks
|
||||
SNTEventStateBlock = 0xFF << 16,
|
||||
@@ -94,7 +97,7 @@ typedef NS_ENUM(NSInteger, SNTEventLogType) {
|
||||
typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
|
||||
SNTMetricFormatTypeUnknown,
|
||||
SNTMetricFormatTypeRawJSON,
|
||||
SNTMetricFormatTypeJSON,
|
||||
SNTMetricFormatTypeMonarchJSON,
|
||||
};
|
||||
|
||||
static const char *kKextPath = "/Library/Extensions/santa-driver.kext";
|
||||
|
||||
@@ -185,6 +185,12 @@
|
||||
|
||||
#pragma mark - GUI Settings
|
||||
|
||||
///
|
||||
/// The text to display when opening Santa.app.
|
||||
/// If unset, the default text will be displayed.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *aboutText;
|
||||
|
||||
///
|
||||
/// The URL to open when the user clicks "More Info..." when opening Santa.app.
|
||||
/// If unset, the button will not be displayed.
|
||||
@@ -248,6 +254,14 @@
|
||||
///
|
||||
@property(readonly, nonatomic) NSURL *syncBaseURL;
|
||||
|
||||
///
|
||||
/// Proxy settings for syncing.
|
||||
/// This dictionary is passed directly to NSURLSession. The allowed keys
|
||||
/// are loosely documented at
|
||||
/// https://developer.apple.com/documentation/cfnetwork/global_proxy_settings_constants.
|
||||
///
|
||||
@property(readonly, nonatomic) NSDictionary *syncProxyConfig;
|
||||
|
||||
///
|
||||
/// The machine owner.
|
||||
///
|
||||
@@ -268,6 +282,23 @@
|
||||
///
|
||||
@property(nonatomic) BOOL syncCleanRequired;
|
||||
|
||||
///
|
||||
/// USB Mount Blocking. Defaults to false.
|
||||
///
|
||||
@property(nonatomic) BOOL blockUSBMount;
|
||||
|
||||
///
|
||||
/// Comma-seperated `$ mount -o` arguments used for forced remounting of USB devices. Default
|
||||
/// to fully allow/deny without remounting if unset.
|
||||
///
|
||||
@property(nonatomic) NSArray<NSString *> *remountUSBMode;
|
||||
|
||||
///
|
||||
/// When `blockUSBMount` is set, this is the message shown to the user when a device is blocked
|
||||
/// If this message is not configured, a reasonable default is provided.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *usbBlockMessage;
|
||||
|
||||
///
|
||||
/// If set, this over-rides the default machine ID used for syncing.
|
||||
///
|
||||
@@ -386,6 +417,15 @@
|
||||
///
|
||||
@property(readonly, nonatomic) NSURL *metricURL;
|
||||
|
||||
///
|
||||
/// Extra Metric Labels to add to the metrics payloads.
|
||||
///
|
||||
@property(readonly, nonatomic) NSDictionary *extraMetricLabels;
|
||||
|
||||
///
|
||||
/// Duration in seconds of how often the metrics should be exported.
|
||||
///
|
||||
@property(readonly, nonatomic) NSUInteger metricExportInterval;
|
||||
|
||||
///
|
||||
/// Retrieve an initialized singleton configurator object using the default file path.
|
||||
|
||||
@@ -45,6 +45,7 @@ static NSString *const kMobileConfigDomain = @"com.google.santa";
|
||||
|
||||
/// The keys managed by a mobileconfig.
|
||||
static NSString *const kSyncBaseURLKey = @"SyncBaseURL";
|
||||
static NSString *const kSyncProxyConfigKey = @"SyncProxyConfiguration";
|
||||
static NSString *const kClientAuthCertificateFileKey = @"ClientAuthCertificateFile";
|
||||
static NSString *const kClientAuthCertificatePasswordKey = @"ClientAuthCertificatePassword";
|
||||
static NSString *const kClientAuthCertificateCNKey = @"ClientAuthCertificateCN";
|
||||
@@ -59,6 +60,7 @@ static NSString *const kMachineOwnerPlistKeyKey = @"MachineOwnerKey";
|
||||
static NSString *const kMachineIDPlistFileKey = @"MachineIDPlist";
|
||||
static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
|
||||
static NSString *const kAboutText = @"AboutText";
|
||||
static NSString *const kMoreInfoURLKey = @"MoreInfoURL";
|
||||
static NSString *const kEventDetailURLKey = @"EventDetailURL";
|
||||
static NSString *const kEventDetailTextKey = @"EventDetailText";
|
||||
@@ -94,6 +96,8 @@ static NSString *const kFCMAPIKey = @"FCMAPIKey";
|
||||
|
||||
// The keys managed by a sync server or mobileconfig.
|
||||
static NSString *const kClientModeKey = @"ClientMode";
|
||||
static NSString *const kBlockUSBMountKey = @"BlockUSBMount";
|
||||
static NSString *const kRemountUSBModeKey = @"RemountUSBMode";
|
||||
static NSString *const kEnableTransitiveRulesKey = @"EnableTransitiveRules";
|
||||
static NSString *const kEnableTransitiveRulesKeyDeprecated = @"EnableTransitiveWhitelisting";
|
||||
static NSString *const kAllowedPathRegexKey = @"AllowedPathRegex";
|
||||
@@ -104,6 +108,8 @@ static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
|
||||
// TODO(markowsky): move these to sync server only.
|
||||
static NSString *const kMetricFormat = @"MetricFormat";
|
||||
static NSString *const kMetricURL = @"MetricURL";
|
||||
static NSString *const kMetricExportInterval = @"MetricExportInterval";
|
||||
static NSString *const kMetricExtraLabels = @"MetricExtraLabels";
|
||||
|
||||
// The keys managed by a sync server.
|
||||
static NSString *const kFullSyncLastSuccess = @"FullSyncLastSuccess";
|
||||
@@ -119,6 +125,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
Class string = [NSString class];
|
||||
Class data = [NSData class];
|
||||
Class array = [NSArray class];
|
||||
Class dictionary = [NSDictionary class];
|
||||
_syncServerKeyTypes = @{
|
||||
kClientModeKey : number,
|
||||
kEnableTransitiveRulesKey : number,
|
||||
@@ -127,6 +134,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kAllowedPathRegexKeyDeprecated : re,
|
||||
kBlockedPathRegexKey : re,
|
||||
kBlockedPathRegexKeyDeprecated : re,
|
||||
kBlockUSBMountKey : number,
|
||||
kRemountUSBModeKey : array,
|
||||
kFullSyncLastSuccess : date,
|
||||
kRuleSyncLastSuccess : date,
|
||||
kSyncCleanRequired : number
|
||||
@@ -141,8 +150,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kAllowedPathRegexKeyDeprecated : re,
|
||||
kBlockedPathRegexKey : re,
|
||||
kBlockedPathRegexKeyDeprecated : re,
|
||||
kBlockUSBMountKey : number,
|
||||
kRemountUSBModeKey : array,
|
||||
kEnablePageZeroProtectionKey : number,
|
||||
kEnableBadSignatureProtectionKey : number,
|
||||
kAboutText : string,
|
||||
kMoreInfoURLKey : string,
|
||||
kEventDetailURLKey : string,
|
||||
kEventDetailTextKey : string,
|
||||
@@ -151,6 +163,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kModeNotificationMonitor : string,
|
||||
kModeNotificationLockdown : string,
|
||||
kSyncBaseURLKey : string,
|
||||
kSyncProxyConfigKey : dictionary,
|
||||
kClientAuthCertificateFileKey : string,
|
||||
kClientAuthCertificatePasswordKey : string,
|
||||
kClientAuthCertificateCNKey : string,
|
||||
@@ -177,6 +190,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kFCMAPIKey : string,
|
||||
kMetricFormat : string,
|
||||
kMetricURL : string,
|
||||
kMetricExportInterval : number,
|
||||
kMetricExtraLabels : dictionary,
|
||||
};
|
||||
_defaults = [NSUserDefaults standardUserDefaults];
|
||||
[_defaults addSuiteNamed:@"com.google.santa"];
|
||||
@@ -256,6 +271,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingAboutText {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingMoreInfoURL {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -384,6 +403,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableBadSignatureProtection {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
#pragma mark Public Interface
|
||||
|
||||
- (SNTClientMode)clientMode {
|
||||
@@ -471,6 +494,20 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return filters;
|
||||
}
|
||||
|
||||
- (void)setRemountUSBMode:(NSArray<NSString *> *)args {
|
||||
[self updateSyncStateForKey:kRemountUSBModeKey value:args];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)remountUSBMode {
|
||||
NSArray<NSString *> *args = self.configState[kRemountUSBModeKey];
|
||||
for (id arg in args) {
|
||||
if (![arg isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
- (NSURL *)syncBaseURL {
|
||||
NSString *urlString = self.configState[kSyncBaseURLKey];
|
||||
if (![urlString hasSuffix:@"/"]) urlString = [urlString stringByAppendingString:@"/"];
|
||||
@@ -478,6 +515,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return url;
|
||||
}
|
||||
|
||||
- (NSDictionary *)syncProxyConfig {
|
||||
return self.configState[kSyncProxyConfigKey];
|
||||
}
|
||||
|
||||
- (BOOL)enablePageZeroProtection {
|
||||
NSNumber *number = self.configState[kEnablePageZeroProtectionKey];
|
||||
return number ? [number boolValue] : YES;
|
||||
@@ -488,6 +529,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (NSString *)aboutText {
|
||||
return self.configState[kAboutText];
|
||||
}
|
||||
|
||||
- (NSURL *)moreInfoURL {
|
||||
return [NSURL URLWithString:self.configState[kMoreInfoURLKey]];
|
||||
}
|
||||
@@ -621,7 +666,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
|
||||
- (BOOL)enableSysxCache {
|
||||
NSNumber *number = self.configState[kEnableSysxCache];
|
||||
return number ? [number boolValue] : NO;
|
||||
return number ? [number boolValue] : YES;
|
||||
}
|
||||
|
||||
- (BOOL)enableForkAndExitLogging {
|
||||
@@ -660,6 +705,15 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return (self.fcmProject.length && self.fcmEntity.length && self.fcmAPIKey.length);
|
||||
}
|
||||
|
||||
- (void)setBlockUSBMount:(BOOL)enabled {
|
||||
[self updateSyncStateForKey:kBlockUSBMountKey value:@(enabled)];
|
||||
}
|
||||
|
||||
- (BOOL)blockUSBMount {
|
||||
NSNumber *number = self.configState[kBlockUSBMountKey];
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns YES if all of the necessary options are set to export metrics, NO
|
||||
/// otherwise.
|
||||
@@ -671,12 +725,13 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
|
||||
- (SNTMetricFormatType)metricFormat {
|
||||
NSString *normalized = [self.configState[kMetricFormat] lowercaseString];
|
||||
|
||||
normalized = [normalized stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
if ([normalized isEqualToString:@"rawjson"]) {
|
||||
return SNTMetricFormatTypeRawJSON;
|
||||
} else if ([normalized isEqualToString:@"json"]) {
|
||||
return SNTMetricFormatTypeJSON;
|
||||
} else if ([normalized isEqualToString:@"monarchjson"]) {
|
||||
return SNTMetricFormatTypeMonarchJSON;
|
||||
} else {
|
||||
return SNTMetricFormatTypeUnknown;
|
||||
}
|
||||
@@ -686,6 +741,20 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [NSURL URLWithString:self.configState[kMetricURL]];
|
||||
}
|
||||
|
||||
// Returns a default value of 30 (for 30 seconds).
|
||||
- (NSUInteger)metricExportInterval {
|
||||
NSNumber *configuredInterval = self.configState[kMetricExportInterval];
|
||||
|
||||
if (configuredInterval == nil) {
|
||||
return 30;
|
||||
}
|
||||
return [configuredInterval unsignedIntegerValue];
|
||||
}
|
||||
|
||||
- (NSDictionary *)extraMetricLabels {
|
||||
return self.configState[kMetricExtraLabels];
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
///
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "SNTCommonEnums.h"
|
||||
|
||||
/**
|
||||
* Provides an abstraction for various metric systems that will be exported to
|
||||
@@ -48,6 +49,8 @@ typedef NS_ENUM(NSInteger, SNTMetricType) {
|
||||
SNTMetricTypeCounter = 9,
|
||||
};
|
||||
|
||||
NSString *SNTMetricMakeStringFromMetricType(SNTMetricType metricType);
|
||||
|
||||
@interface SNTMetric : NSObject
|
||||
- (NSDictionary *)export;
|
||||
@end
|
||||
@@ -100,8 +103,21 @@ typedef NS_ENUM(NSInteger, SNTMetricType) {
|
||||
fieldNames:(NSArray<NSString *> *)fieldNames
|
||||
helpText:(NSString *)text;
|
||||
|
||||
/**
|
||||
* Returns a shared global instance with default root labels and metrics registered.
|
||||
*/
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
/**
|
||||
* Add a root label to the MetricSet.
|
||||
*/
|
||||
- (void)addRootLabel:(NSString *)label value:(NSString *)value;
|
||||
|
||||
/**
|
||||
* Remove a root label from the MetricSet.
|
||||
*/
|
||||
- (void)removeRootLabel:(NSString *)labelName;
|
||||
|
||||
/**
|
||||
* Returns a int64 gauge metric with the given Streamz name and help text,
|
||||
* registered with this MetricSet.
|
||||
@@ -170,4 +186,12 @@ typedef NS_ENUM(NSInteger, SNTMetricType) {
|
||||
- (NSDictionary *)export;
|
||||
@end
|
||||
|
||||
// Returns a human readble string from an SNTMetricFormat type
|
||||
NSString *SNTMetricStringFromMetricFormatType(SNTMetricFormatType format);
|
||||
|
||||
/** Normalizes dates in an exported dictionary to be ISO8601 timestamp strings in
|
||||
* UTC time.
|
||||
*/
|
||||
NSDictionary *SNTMetricConvertDatesToISO8601Strings(NSDictionary *metrics);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -13,6 +13,24 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "SNTMetricSet.h"
|
||||
#import "SNTCommonEnums.h"
|
||||
|
||||
NSString *SNTMetricMakeStringFromMetricType(SNTMetricType metricType) {
|
||||
NSString *typeStr;
|
||||
switch (metricType) {
|
||||
case SNTMetricTypeConstantBool: typeStr = @"SNTMetricTypeConstantBool"; break;
|
||||
case SNTMetricTypeConstantString: typeStr = @"SNTMetricTypeConstantString"; break;
|
||||
case SNTMetricTypeConstantInt64: typeStr = @"SNTMetricTypeConstantInt64"; break;
|
||||
case SNTMetricTypeConstantDouble: typeStr = @"SNTMetricTypeConstantDouble"; break;
|
||||
case SNTMetricTypeGaugeBool: typeStr = @"SNTMetricTypeGaugeBool"; break;
|
||||
case SNTMetricTypeGaugeString: typeStr = @"SNTMetricTypeGaugeString"; break;
|
||||
case SNTMetricTypeGaugeInt64: typeStr = @"SNTMetricTypeGaugeInt64"; break;
|
||||
case SNTMetricTypeGaugeDouble: typeStr = @"SNTMetricTypeGaugeDouble"; break;
|
||||
case SNTMetricTypeCounter: typeStr = @"SNTMetricTypeCounter"; break;
|
||||
default: typeStr = @"SNTMetricTypeUnknown"; break;
|
||||
}
|
||||
return [NSString stringWithFormat:@"%@ %ld", typeStr, metricType];
|
||||
}
|
||||
|
||||
/**
|
||||
* SNTMetricValue encapsulates the value of a metric along with the creation
|
||||
@@ -257,6 +275,7 @@
|
||||
NSMutableDictionary *metricDict = [NSMutableDictionary dictionaryWithCapacity:_fieldNames.count];
|
||||
metricDict[@"type"] = [NSNumber numberWithInt:(int)_type];
|
||||
metricDict[@"fields"] = [[NSMutableDictionary alloc] init];
|
||||
metricDict[@"description"] = [_help copy];
|
||||
|
||||
if (_fieldNames.count == 0) {
|
||||
metricDict[@"fields"][@""] = @[ [self encodeMetricValueForFieldValues:@[]] ];
|
||||
@@ -431,6 +450,17 @@
|
||||
NSMutableArray<void (^)(void)> *_callbacks;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static SNTMetricSet *sharedMetrics;
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedMetrics = [[SNTMetricSet alloc] init];
|
||||
});
|
||||
|
||||
return sharedMetrics;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
@@ -461,6 +491,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeRootLabel:(NSString *)label {
|
||||
@synchronized(self) {
|
||||
[_rootLabels removeObjectForKey:label];
|
||||
}
|
||||
}
|
||||
|
||||
- (SNTMetric *)registerMetric:(nonnull SNTMetric *)metric {
|
||||
@synchronized(self) {
|
||||
SNTMetric *oldMetric = _metrics[[metric name]];
|
||||
@@ -485,8 +521,7 @@
|
||||
SNTMetricCounter *c = [[SNTMetricCounter alloc] initWithName:name
|
||||
fieldNames:fieldNames
|
||||
helpText:helpText];
|
||||
[self registerMetric:c];
|
||||
return c;
|
||||
return (SNTMetricCounter *)[self registerMetric:c];
|
||||
}
|
||||
|
||||
- (SNTMetricInt64Gauge *)int64GaugeWithName:(NSString *)name
|
||||
@@ -495,8 +530,7 @@
|
||||
SNTMetricInt64Gauge *g = [[SNTMetricInt64Gauge alloc] initWithName:name
|
||||
fieldNames:fieldNames
|
||||
helpText:helpText];
|
||||
[self registerMetric:g];
|
||||
return g;
|
||||
return (SNTMetricInt64Gauge *)[self registerMetric:g];
|
||||
}
|
||||
|
||||
- (SNTMetricDoubleGauge *)doubleGaugeWithName:(NSString *)name
|
||||
@@ -506,8 +540,7 @@
|
||||
fieldNames:fieldNames
|
||||
helpText:helpText];
|
||||
|
||||
[self registerMetric:g];
|
||||
return g;
|
||||
return (SNTMetricDoubleGauge *)[self registerMetric:g];
|
||||
}
|
||||
|
||||
- (SNTMetricStringGauge *)stringGaugeWithName:(NSString *)name
|
||||
@@ -517,8 +550,7 @@
|
||||
fieldNames:fieldNames
|
||||
helpText:helpText];
|
||||
|
||||
[self registerMetric:s];
|
||||
return s;
|
||||
return (SNTMetricStringGauge *)[self registerMetric:s];
|
||||
}
|
||||
|
||||
- (SNTMetricBooleanGauge *)booleanGaugeWithName:(NSString *)name
|
||||
@@ -528,8 +560,7 @@
|
||||
fieldNames:fieldNames
|
||||
helpText:helpText];
|
||||
|
||||
[self registerMetric:b];
|
||||
return b;
|
||||
return (SNTMetricBooleanGauge *)[self registerMetric:b];
|
||||
}
|
||||
|
||||
- (void)addConstantStringWithName:(NSString *)name
|
||||
@@ -581,19 +612,61 @@
|
||||
}
|
||||
|
||||
@synchronized(self) {
|
||||
// Walk root labels
|
||||
NSMutableDictionary *exportDict = [[NSMutableDictionary alloc] init];
|
||||
exportDict[@"root_labels"] = [NSDictionary dictionaryWithDictionary:_rootLabels];
|
||||
exportDict[@"root_labels"] = [_rootLabels copy];
|
||||
exportDict[@"metrics"] = [[NSMutableDictionary alloc] init];
|
||||
|
||||
// sort the metrics so we always get the same output.
|
||||
for (id metricName in _metrics) {
|
||||
SNTMetric *metric = [_metrics objectForKey:metricName];
|
||||
exportDict[@"metrics"][metricName] = [metric export];
|
||||
// TODO(markowsky) Sort the metrics so we always get the same output.
|
||||
for (NSString *metricName in _metrics) {
|
||||
exportDict[@"metrics"][metricName] = [_metrics[metricName] export];
|
||||
}
|
||||
|
||||
exported = [NSDictionary dictionaryWithDictionary:exportDict];
|
||||
}
|
||||
return exported;
|
||||
}
|
||||
|
||||
// Returns a human readble string from an SNTMetricFormat type
|
||||
NSString *SNTMetricStringFromMetricFormatType(SNTMetricFormatType format) {
|
||||
switch (format) {
|
||||
case SNTMetricFormatTypeRawJSON: return @"rawjson";
|
||||
case SNTMetricFormatTypeMonarchJSON: return @"monarchjson";
|
||||
default: return @"Unknown Metric Format";
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *SNTMetricConvertDatesToISO8601Strings(NSDictionary *metrics) {
|
||||
NSMutableDictionary *mutableMetrics = [metrics mutableCopy];
|
||||
|
||||
id formatter;
|
||||
|
||||
if (@available(macOS 10.13, *)) {
|
||||
NSISO8601DateFormatter *isoFormatter = [[NSISO8601DateFormatter alloc] init];
|
||||
|
||||
isoFormatter.formatOptions =
|
||||
NSISO8601DateFormatWithInternetDateTime | NSISO8601DateFormatWithFractionalSeconds;
|
||||
formatter = isoFormatter;
|
||||
} else {
|
||||
NSDateFormatter *localFormatter = [[NSDateFormatter alloc] init];
|
||||
[localFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
|
||||
[localFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
|
||||
formatter = localFormatter;
|
||||
}
|
||||
|
||||
for (NSString *metricName in mutableMetrics[@"metrics"]) {
|
||||
NSMutableDictionary *metric = mutableMetrics[@"metrics"][metricName];
|
||||
|
||||
for (NSString *field in metric[@"fields"]) {
|
||||
NSMutableArray<NSMutableDictionary *> *values = metric[@"fields"][field];
|
||||
|
||||
[values enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
|
||||
values[index][@"created"] = [formatter stringFromDate:values[index][@"created"]];
|
||||
values[index][@"last_updated"] = [formatter stringFromDate:values[index][@"last_updated"]];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
return mutableMetrics;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
@interface SNTMetricSetTest : XCTestCase
|
||||
@end
|
||||
|
||||
@interface SNTMetricSetHelperFunctionsTest : XCTestCase
|
||||
@end
|
||||
|
||||
// Stub out NSDate's date method
|
||||
@implementation NSDate (custom)
|
||||
|
||||
@@ -60,6 +63,7 @@
|
||||
|
||||
NSDictionary *expected = @{
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeCounter],
|
||||
@"description" : @"Count of exec events broken out by rule type.",
|
||||
@"fields" : @{
|
||||
@"rule_type" : @[ @{
|
||||
@"value" : @"certificate",
|
||||
@@ -72,6 +76,19 @@
|
||||
|
||||
XCTAssertEqualObjects([c export], expected);
|
||||
}
|
||||
|
||||
- (void)testAddingMetricWithSameSchema {
|
||||
SNTMetricSet *metricSet = [[SNTMetricSet alloc] init];
|
||||
SNTMetricCounter *a = [metricSet counterWithName:@"/santa/counter"
|
||||
fieldNames:@[]
|
||||
helpText:@"Test counter."];
|
||||
|
||||
SNTMetricCounter *b = [metricSet counterWithName:@"/santa/counter"
|
||||
fieldNames:@[]
|
||||
helpText:@"Test counter."];
|
||||
|
||||
XCTAssertEqual(a, b, @"Unexpected new counter returned.");
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation SNTMetricBooleanGaugeTest
|
||||
@@ -96,6 +113,7 @@
|
||||
[b set:true forFieldValues:@[]];
|
||||
NSDictionary *expected = @{
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeBool],
|
||||
@"description" : @"Is the daemon connected.",
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@"value" : @"",
|
||||
@@ -109,6 +127,20 @@
|
||||
NSDictionary *output = [b export];
|
||||
XCTAssertEqualObjects(output, expected);
|
||||
}
|
||||
|
||||
- (void)testAddingBooleanWithSameSchema {
|
||||
SNTMetricSet *metricSet = [[SNTMetricSet alloc] init];
|
||||
SNTMetricBooleanGauge *a = [metricSet booleanGaugeWithName:@"/santa/daemon_connected"
|
||||
fieldNames:@[]
|
||||
helpText:@"Is the daemon connected."];
|
||||
|
||||
SNTMetricBooleanGauge *b = [metricSet booleanGaugeWithName:@"/santa/daemon_connected"
|
||||
fieldNames:@[]
|
||||
helpText:@"Is the daemon connected."];
|
||||
|
||||
XCTAssertEqual(a, b, @"Unexpected new boolean gauge returned.");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation SNTMetricGaugeInt64Test
|
||||
@@ -150,6 +182,7 @@
|
||||
|
||||
NSDictionary *expected = @{
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeInt64],
|
||||
@"description" : @"Count of rules broken out by rule type.",
|
||||
@"fields" : @{
|
||||
@"rule_type" : @[ @{
|
||||
@"value" : @"binary",
|
||||
@@ -162,6 +195,20 @@
|
||||
|
||||
XCTAssertEqualObjects([g export], expected);
|
||||
}
|
||||
|
||||
- (void)testAddingMetricWithSameSchema {
|
||||
SNTMetricSet *metricSet = [[SNTMetricSet alloc] init];
|
||||
SNTMetricInt64Gauge *a = [metricSet int64GaugeWithName:@"/santa/int64gauge"
|
||||
fieldNames:@[]
|
||||
helpText:@"Test gauge."];
|
||||
|
||||
SNTMetricInt64Gauge *b = [metricSet int64GaugeWithName:@"/santa/int64gauge"
|
||||
fieldNames:@[]
|
||||
helpText:@"Test gauge."];
|
||||
|
||||
XCTAssertEqual(a, b, @"Unexpected new gauge returned.");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation SNTMetricDoubleGaugeTest
|
||||
@@ -201,6 +248,7 @@
|
||||
|
||||
NSDictionary *expected = @{
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeDouble],
|
||||
@"description" : @"CPU time consumed by this process.",
|
||||
@"fields" : @{
|
||||
@"mode" : @[
|
||||
@{
|
||||
@@ -220,6 +268,19 @@
|
||||
};
|
||||
XCTAssertEqualObjects([g export], expected);
|
||||
}
|
||||
|
||||
- (void)testAddingMetricWithSameSchema {
|
||||
SNTMetricSet *metricSet = [[SNTMetricSet alloc] init];
|
||||
SNTMetricDoubleGauge *a = [metricSet doubleGaugeWithName:@"/santa/doublegauge"
|
||||
fieldNames:@[]
|
||||
helpText:@"Test gauge."];
|
||||
|
||||
SNTMetricDoubleGauge *b = [metricSet doubleGaugeWithName:@"/santa/doublegauge"
|
||||
fieldNames:@[]
|
||||
helpText:@"Test gauge."];
|
||||
|
||||
XCTAssertEqual(a, b, @"Unexpected new gauge returned.");
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation SNTMetricStringGaugeTest
|
||||
@@ -233,6 +294,7 @@
|
||||
[s set:@"testValue" forFieldValues:@[]];
|
||||
XCTAssertEqualObjects([s getStringValueForFieldValues:@[]], @"testValue");
|
||||
}
|
||||
|
||||
- (void)testExportNSDictionary {
|
||||
SNTMetricSet *metricSet = [[SNTMetricSet alloc] init];
|
||||
SNTMetricStringGauge *s = [metricSet stringGaugeWithName:@"/santa/mode"
|
||||
@@ -244,6 +306,7 @@
|
||||
|
||||
NSDictionary *expected = @{
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeString],
|
||||
@"description" : @"String description of the mode.",
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@"value" : @"",
|
||||
@@ -256,6 +319,20 @@
|
||||
|
||||
XCTAssertEqualObjects([s export], expected);
|
||||
}
|
||||
|
||||
- (void)testAddingMetricWithSameSchema {
|
||||
SNTMetricSet *metricSet = [[SNTMetricSet alloc] init];
|
||||
SNTMetricStringGauge *a = [metricSet stringGaugeWithName:@"/santa/stringgauge"
|
||||
fieldNames:@[]
|
||||
helpText:@"Test gauge."];
|
||||
|
||||
SNTMetricStringGauge *b = [metricSet stringGaugeWithName:@"/santa/stringgauge"
|
||||
fieldNames:@[]
|
||||
helpText:@"Test gauge."];
|
||||
|
||||
XCTAssertEqual(a, b, @"Unexpected new gauge returned.");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation SNTMetricSetTest
|
||||
@@ -265,8 +342,18 @@
|
||||
|
||||
NSDictionary *expected = @{@"root_labels" : @{@"hostname" : @"localhost"}, @"metrics" : @{}};
|
||||
|
||||
NSDictionary *output = [metricSet export];
|
||||
XCTAssertEqualObjects(output, expected);
|
||||
XCTAssertEqualObjects(expected, [metricSet export]);
|
||||
|
||||
// ensure that adding a rootLabel with the same name overwrites.
|
||||
expected = @{@"root_labels" : @{@"hostname" : @"localhost2"}, @"metrics" : @{}};
|
||||
[metricSet addRootLabel:@"hostname" value:@"localhost2"];
|
||||
|
||||
XCTAssertEqualObjects(expected, [metricSet export],
|
||||
@"failed to overwrite rootLabel with second call to addRootLabel");
|
||||
|
||||
// ensure that removing a rootLabelWorks
|
||||
expected = @{@"root_labels" : @{}, @"metrics" : @{}};
|
||||
[metricSet removeRootLabel:@"hostname"];
|
||||
}
|
||||
|
||||
- (void)testDoubleRegisteringIncompatibleMetricsFails {
|
||||
@@ -313,6 +400,7 @@
|
||||
|
||||
NSDictionary *expected = @{
|
||||
@"/tautology" : @{
|
||||
@"description" : @"The first rule of tautology club is the first rule",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantBool],
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@@ -337,6 +425,7 @@
|
||||
|
||||
NSDictionary *expected = @{
|
||||
@"/build/label" : @{
|
||||
@"description" : @"Build label for the binary",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantString],
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@@ -360,6 +449,7 @@
|
||||
|
||||
NSDictionary *expected = @{
|
||||
@"/deep/thought/answer" : @{
|
||||
@"description" : @"Life, the universe, and everything",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantInt64],
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@@ -386,10 +476,10 @@
|
||||
[metricSet addConstantBooleanWithName:@"/santa/using_endpoint_security_framework"
|
||||
helpText:@"Is santad using the endpoint security framework."
|
||||
value:TRUE];
|
||||
[metricSet addConstantIntegerWithName:@"/proc/birth_timestamp"
|
||||
helpText:@"Start time of this LogDumper instance, in microseconds "
|
||||
@"since epoch"
|
||||
value:(long long)(0x12345668910)];
|
||||
[metricSet
|
||||
addConstantIntegerWithName:@"/proc/birth_timestamp"
|
||||
helpText:@"Start time of this santad instance, in microseconds since epoch"
|
||||
value:(long long)(0x12345668910)];
|
||||
// Add Metrics
|
||||
SNTMetricCounter *c = [metricSet counterWithName:@"/santa/events"
|
||||
fieldNames:@[ @"rule_type" ]
|
||||
@@ -414,7 +504,7 @@
|
||||
SNTMetricInt64Gauge *residentMemoryGauge =
|
||||
[metricSet int64GaugeWithName:@"/proc/memory/resident_size"
|
||||
fieldNames:@[]
|
||||
helpText:@"The resident set siz of this process."];
|
||||
helpText:@"The resident set size of this process."];
|
||||
|
||||
[metricSet registerCallback:^(void) {
|
||||
[virtualMemoryGauge set:987654321 forFieldValues:@[]];
|
||||
@@ -425,6 +515,7 @@
|
||||
@"root_labels" : @{@"hostname" : @"testHost", @"username" : @"testUser"},
|
||||
@"metrics" : @{
|
||||
@"/build/label" : @{
|
||||
@"description" : @"Software version running.",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantString],
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@@ -436,6 +527,7 @@
|
||||
}
|
||||
},
|
||||
@"/santa/events" : @{
|
||||
@"description" : @"Count of events on the host",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeCounter],
|
||||
@"fields" : @{
|
||||
@"rule_type" : @[
|
||||
@@ -455,6 +547,7 @@
|
||||
},
|
||||
},
|
||||
@"/santa/rules" : @{
|
||||
@"description" : @"Number of rules.",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeInt64],
|
||||
@"fields" : @{
|
||||
@"rule_type" : @[
|
||||
@@ -474,6 +567,7 @@
|
||||
},
|
||||
},
|
||||
@"/santa/using_endpoint_security_framework" : @{
|
||||
@"description" : @"Is santad using the endpoint security framework.",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantBool],
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@@ -485,6 +579,7 @@
|
||||
}
|
||||
},
|
||||
@"/proc/birth_timestamp" : @{
|
||||
@"description" : @"Start time of this santad instance, in microseconds since epoch",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeConstantInt64],
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@@ -496,6 +591,7 @@
|
||||
},
|
||||
},
|
||||
@"/proc/memory/virtual_size" : @{
|
||||
@"description" : @"The virtual memory size of this process.",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeInt64],
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@@ -507,6 +603,7 @@
|
||||
}
|
||||
},
|
||||
@"/proc/memory/resident_size" : @{
|
||||
@"description" : @"The resident set size of this process.",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeGaugeInt64],
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@@ -522,5 +619,57 @@
|
||||
|
||||
XCTAssertEqualObjects([metricSet export], expected);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation SNTMetricSetHelperFunctionsTest
|
||||
- (void)testMakeMetricString {
|
||||
NSArray<NSDictionary *> *tests = @[
|
||||
@{
|
||||
@"input" : [NSNumber numberWithInt:SNTMetricTypeUnknown],
|
||||
@"expected" : @"SNTMetricTypeUnknown 0"
|
||||
},
|
||||
@{
|
||||
@"input" : [NSNumber numberWithInt:SNTMetricTypeConstantBool],
|
||||
@"expected" : @"SNTMetricTypeConstantBool 1"
|
||||
},
|
||||
@{
|
||||
@"input" : [NSNumber numberWithInt:SNTMetricTypeConstantString],
|
||||
@"expected" : @"SNTMetricTypeConstantString 2"
|
||||
},
|
||||
@{
|
||||
@"input" : [NSNumber numberWithInt:SNTMetricTypeConstantInt64],
|
||||
@"expected" : @"SNTMetricTypeConstantInt64 3"
|
||||
},
|
||||
@{
|
||||
@"input" : [NSNumber numberWithInt:SNTMetricTypeConstantDouble],
|
||||
@"expected" : @"SNTMetricTypeConstantDouble 4"
|
||||
},
|
||||
@{
|
||||
@"input" : [NSNumber numberWithInt:SNTMetricTypeGaugeBool],
|
||||
@"expected" : @"SNTMetricTypeGaugeBool 5"
|
||||
},
|
||||
@{
|
||||
@"input" : [NSNumber numberWithInt:SNTMetricTypeGaugeString],
|
||||
@"expected" : @"SNTMetricTypeGaugeString 6"
|
||||
},
|
||||
@{
|
||||
@"input" : [NSNumber numberWithInt:SNTMetricTypeGaugeInt64],
|
||||
@"expected" : @"SNTMetricTypeGaugeInt64 7"
|
||||
},
|
||||
@{
|
||||
@"input" : [NSNumber numberWithInt:SNTMetricTypeGaugeDouble],
|
||||
@"expected" : @"SNTMetricTypeGaugeDouble 8"
|
||||
},
|
||||
@{
|
||||
@"input" : [NSNumber numberWithInt:SNTMetricTypeCounter],
|
||||
@"expected" : @"SNTMetricTypeCounter 9"
|
||||
}
|
||||
];
|
||||
|
||||
for (NSDictionary *test in tests) {
|
||||
NSString *output = SNTMetricMakeStringFromMetricType([test[@"input"] integerValue]);
|
||||
XCTAssertEqualObjects(test[@"expected"], output, @"expected %@ got %@", test[@"expected"],
|
||||
output);
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -45,26 +45,29 @@
|
||||
[UUIDs addObject:[NSUUID UUID].UUIDString];
|
||||
}
|
||||
|
||||
__block BOOL stop = NO;
|
||||
|
||||
// Create a bunch of background noise.
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
dispatch_apply(UINT64_MAX, dispatch_get_global_queue(0, 0), ^(size_t i) {
|
||||
t->HasPrefix([UUIDs[i % count] UTF8String]);
|
||||
});
|
||||
for (uint64_t i = 0; i < UINT64_MAX; ++i) {
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
t->HasPrefix([UUIDs[i % count] UTF8String]);
|
||||
});
|
||||
if (stop) return;
|
||||
}
|
||||
});
|
||||
|
||||
// Fill up the tree.
|
||||
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
|
||||
if (t->AddPrefix([UUIDs[i] UTF8String]) != kIOReturnSuccess) {
|
||||
XCTFail();
|
||||
}
|
||||
XCTAssertEqual(t->AddPrefix([UUIDs[i] UTF8String]), kIOReturnSuccess);
|
||||
});
|
||||
|
||||
// Make sure every leaf byte is found.
|
||||
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
|
||||
if (!t->HasPrefix([UUIDs[i] UTF8String])) {
|
||||
XCTFail();
|
||||
}
|
||||
XCTAssertTrue(t->HasPrefix([UUIDs[i] UTF8String]));
|
||||
});
|
||||
|
||||
stop = YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
///
|
||||
/// The hash of the object this rule is for
|
||||
///
|
||||
@property(copy) NSString *shasum;
|
||||
@property(copy) NSString *identifier;
|
||||
|
||||
///
|
||||
/// The state of this rule
|
||||
@@ -50,7 +50,7 @@
|
||||
///
|
||||
/// Designated initializer.
|
||||
///
|
||||
- (instancetype)initWithShasum:(NSString *)shasum
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg
|
||||
@@ -59,7 +59,7 @@
|
||||
///
|
||||
/// Initialize with a default timestamp: current time if rule state is transitive, 0 otherwise.
|
||||
///
|
||||
- (instancetype)initWithShasum:(NSString *)shasum
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg;
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
|
||||
@implementation SNTRule
|
||||
|
||||
- (instancetype)initWithShasum:(NSString *)shasum
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg
|
||||
timestamp:(NSUInteger)timestamp {
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg
|
||||
timestamp:(NSUInteger)timestamp {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_shasum = shasum;
|
||||
_identifier = identifier;
|
||||
_state = state;
|
||||
_type = type;
|
||||
_customMsg = customMsg;
|
||||
@@ -36,11 +36,11 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithShasum:(NSString *)shasum
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg {
|
||||
self = [self initWithShasum:shasum state:state type:type customMsg:customMsg timestamp:0];
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg {
|
||||
self = [self initWithIdentifier:identifier state:state type:type customMsg:customMsg timestamp:0];
|
||||
// Initialize timestamp to current time if rule is transitive.
|
||||
if (self && state == SNTRuleStateAllowTransitive) {
|
||||
[self resetTimestamp];
|
||||
@@ -61,7 +61,7 @@
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
ENCODE(self.shasum, @"shasum");
|
||||
ENCODE(self.identifier, @"identifier");
|
||||
ENCODE(@(self.state), @"state");
|
||||
ENCODE(@(self.type), @"type");
|
||||
ENCODE(self.customMsg, @"custommsg");
|
||||
@@ -71,7 +71,7 @@
|
||||
- (instancetype)initWithCoder:(NSCoder *)decoder {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_shasum = DECODE(NSString, @"shasum");
|
||||
_identifier = DECODE(NSString, @"identifier");
|
||||
_state = [DECODE(NSNumber, @"state") intValue];
|
||||
_type = [DECODE(NSNumber, @"type") intValue];
|
||||
_customMsg = DECODE(NSString, @"custommsg");
|
||||
@@ -88,22 +88,22 @@
|
||||
if (other == self) return YES;
|
||||
if (![other isKindOfClass:[SNTRule class]]) return NO;
|
||||
SNTRule *o = other;
|
||||
return ([self.shasum isEqual:o.shasum] && self.state == o.state && self.type == o.type);
|
||||
return ([self.identifier isEqual:o.identifier] && self.state == o.state && self.type == o.type);
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
NSUInteger prime = 31;
|
||||
NSUInteger result = 1;
|
||||
result = prime * result + [self.shasum hash];
|
||||
result = prime * result + [self.identifier hash];
|
||||
result = prime * result + self.state;
|
||||
result = prime * result + self.type;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return
|
||||
[NSString stringWithFormat:@"SNTRule: SHA-256: %@, State: %ld, Type: %ld, Timestamp: %lu",
|
||||
self.shasum, self.state, self.type, (unsigned long)self.timestamp];
|
||||
return [NSString
|
||||
stringWithFormat:@"SNTRule: Identifier: %@, State: %ld, Type: %ld, Timestamp: %lu",
|
||||
self.identifier, self.state, self.type, (unsigned long)self.timestamp];
|
||||
}
|
||||
|
||||
#pragma mark Last-access Timestamp
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
- (void)databaseRemoveEventsWithIDs:(NSArray *)ids;
|
||||
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
reply:(void (^)(SNTRule *))reply;
|
||||
|
||||
///
|
||||
@@ -46,6 +47,8 @@
|
||||
- (void)setSyncCleanRequired:(BOOL)cleanReqd reply:(void (^)(void))reply;
|
||||
- (void)setAllowedPathRegex:(NSString *)pattern reply:(void (^)(void))reply;
|
||||
- (void)setBlockedPathRegex:(NSString *)pattern reply:(void (^)(void))reply;
|
||||
- (void)setBlockUSBMount:(BOOL)enabled reply:(void (^)(void))reply;
|
||||
- (void)setRemountUSBMode:(NSArray *)remountUSBMode reply:(void (^)(void))reply;
|
||||
- (void)setEnableBundles:(BOOL)bundlesEnabled reply:(void (^)(void))reply;
|
||||
- (void)setEnableTransitiveRules:(BOOL)enabled reply:(void (^)(void))reply;
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
|
||||
#import "Source/common/SNTXPCMetricServiceInterface.h"
|
||||
|
||||
|
||||
@implementation SNTXPCMetricServiceInterface
|
||||
|
||||
+ (NSXPCInterface *)metricServiceInterface {
|
||||
NSXPCInterface *r = [NSXPCInterface interfaceWithProtocol:@protocol(SNTMetricServiceXPC)];
|
||||
|
||||
[r setClasses:[NSSet setWithObjects:[NSDictionary class], nil]
|
||||
[r setClasses:[NSSet setWithObjects:[NSDictionary class], [NSArray class], [NSNumber class],
|
||||
[NSString class], [NSDate class], nil]
|
||||
forSelector:@selector(exportForMonitoring:)
|
||||
argumentIndex:0
|
||||
ofReply:NO];
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
/// Database ops
|
||||
///
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive))reply;
|
||||
int64_t transitive, int64_t teamID))reply;
|
||||
- (void)databaseEventCount:(void (^)(int64_t count))reply;
|
||||
|
||||
///
|
||||
@@ -57,6 +57,7 @@
|
||||
- (void)decisionForFilePath:(NSString *)filePath
|
||||
fileSHA256:(NSString *)fileSHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
reply:(void (^)(SNTEventState))reply;
|
||||
|
||||
///
|
||||
@@ -71,6 +72,11 @@
|
||||
- (void)enableBundles:(void (^)(BOOL))reply;
|
||||
- (void)enableTransitiveRules:(void (^)(BOOL))reply;
|
||||
|
||||
///
|
||||
/// Metrics ops
|
||||
///
|
||||
- (void)metrics:(void (^)(NSDictionary *))reply;
|
||||
|
||||
///
|
||||
/// GUI Ops
|
||||
///
|
||||
|
||||
@@ -48,19 +48,20 @@ macos_application(
|
||||
additional_contents = {
|
||||
"//Source/santactl": "MacOS",
|
||||
"//Source/santabundleservice": "MacOS",
|
||||
"//Source/santametricservice": "MacOS",
|
||||
"//Source/santad:com.google.santa.daemon": "Library/SystemExtensions",
|
||||
},
|
||||
app_icons = glob(["Resources/Images.xcassets/**"]),
|
||||
bundle_id = "com.google.santa",
|
||||
bundle_name = "Santa",
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "10.9",
|
||||
entitlements = "Santa.app.entitlements",
|
||||
codesignopts = [
|
||||
"--timestamp",
|
||||
"--force",
|
||||
"--options library,kill,runtime",
|
||||
],
|
||||
entitlements = "Santa.app.entitlements",
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "10.9",
|
||||
provisioning_profile = select({
|
||||
"//:ci_build": None,
|
||||
"//conditions:default": "Santa_Dev.provisionprofile",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="SNTAboutWindowController">
|
||||
<connections>
|
||||
<outlet property="aboutTextField" destination="uh6-q0-RzL" id="oGn-hV-wwc"/>
|
||||
<outlet property="moreInfoButton" destination="SRu-Kf-vu5" id="Vj2-9Q-05d"/>
|
||||
<outlet property="window" destination="F0z-JX-Cv5" id="gIp-Ho-8D9"/>
|
||||
</connections>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
@interface SNTAboutWindowController : NSWindowController
|
||||
|
||||
@property IBOutlet NSTextField *aboutTextField;
|
||||
@property IBOutlet NSButton *moreInfoButton;
|
||||
|
||||
- (IBAction)openMoreInfoURL:(id)sender;
|
||||
|
||||
@@ -24,7 +24,12 @@
|
||||
|
||||
- (void)loadWindow {
|
||||
[super loadWindow];
|
||||
if (![[SNTConfigurator configurator] moreInfoURL]) {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
NSString *aboutText = [config aboutText];
|
||||
if (aboutText) {
|
||||
[self.aboutTextField setStringValue:aboutText];
|
||||
}
|
||||
if (![config moreInfoURL]) {
|
||||
[self.moreInfoButton removeFromSuperview];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.application-identifier</key>
|
||||
<string>EQHXZ8M8AV.com.google.santa</string>
|
||||
<key>com.apple.developer.system-extension.install</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>EQHXZ8M8AV</string>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>EQHXZ8M8AV.com.google.santa</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -32,10 +32,10 @@ cc_library(
|
||||
"SANTA_VERSION=" + SANTA_VERSION,
|
||||
],
|
||||
deps = [
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SNTKernelCommon",
|
||||
"//Source/common:SNTLoggingKernel",
|
||||
"//Source/common:SNTPrefixTreeKernel",
|
||||
"//Source/common:SantaCache",
|
||||
],
|
||||
alwayslink = 1,
|
||||
)
|
||||
|
||||
@@ -3,6 +3,8 @@ load("//:helper.bzl", "santa_unit_test")
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
package(default_visibility = ["//:santa_package_group"])
|
||||
|
||||
objc_library(
|
||||
name = "santactl_lib",
|
||||
srcs = [
|
||||
@@ -15,25 +17,9 @@ objc_library(
|
||||
"Commands/SNTCommandRule.m",
|
||||
"Commands/SNTCommandStatus.m",
|
||||
"Commands/SNTCommandVersion.m",
|
||||
"Commands/sync/NSData+Zlib.h",
|
||||
"Commands/sync/NSData+Zlib.m",
|
||||
"Commands/sync/SNTCommandSync.m",
|
||||
"Commands/sync/SNTCommandSyncConstants.h",
|
||||
"Commands/sync/SNTCommandSyncConstants.m",
|
||||
"Commands/sync/SNTCommandSyncEventUpload.h",
|
||||
"Commands/sync/SNTCommandSyncEventUpload.m",
|
||||
"Commands/sync/SNTCommandSyncManager.h",
|
||||
"Commands/sync/SNTCommandSyncManager.m",
|
||||
"Commands/sync/SNTCommandSyncPostflight.h",
|
||||
"Commands/sync/SNTCommandSyncPostflight.m",
|
||||
"Commands/sync/SNTCommandSyncPreflight.h",
|
||||
"Commands/sync/SNTCommandSyncPreflight.m",
|
||||
"Commands/sync/SNTCommandSyncRuleDownload.h",
|
||||
"Commands/sync/SNTCommandSyncRuleDownload.m",
|
||||
"Commands/sync/SNTCommandSyncStage.h",
|
||||
"Commands/sync/SNTCommandSyncStage.m",
|
||||
"Commands/sync/SNTCommandSyncState.h",
|
||||
"Commands/sync/SNTCommandSyncState.m",
|
||||
"Commands/SNTCommandMetrics.h",
|
||||
"Commands/SNTCommandMetrics.m",
|
||||
"Commands/SNTCommandSync.m",
|
||||
] + select({
|
||||
"//:opt_build": [],
|
||||
"//conditions:default": [
|
||||
@@ -46,7 +32,6 @@ objc_library(
|
||||
sdk_dylibs = ["libz"],
|
||||
sdk_frameworks = ["IOKit"],
|
||||
deps = [
|
||||
":FCM_lib",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -54,43 +39,36 @@ objc_library(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTKernelCommon",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTStrengthify",
|
||||
"//Source/common:SNTSystemInfo",
|
||||
"//Source/common:SNTXPCBundleServiceInterface",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"//Source/common:SNTXPCSyncdInterface",
|
||||
"//Source/common:SNTXPCUnprivilegedControlInterface",
|
||||
"//Source/santasyncservice:sync_lib",
|
||||
"@FMDB",
|
||||
"@MOLAuthenticatingURLSession",
|
||||
"@MOLCodesignChecker",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "FCM_lib",
|
||||
srcs = ["Commands/sync/SNTCommandSyncFCM.m"],
|
||||
hdrs = ["Commands/sync/SNTCommandSyncFCM.h"],
|
||||
sdk_frameworks = ["SystemConfiguration"],
|
||||
deps = [
|
||||
"@MOLAuthenticatingURLSession",
|
||||
],
|
||||
)
|
||||
|
||||
macos_command_line_application(
|
||||
name = "santactl",
|
||||
bundle_id = "com.google.santa.ctl",
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "10.9",
|
||||
codesignopts = [
|
||||
"--timestamp",
|
||||
"--force",
|
||||
"--options library,kill,runtime",
|
||||
],
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "10.9",
|
||||
provisioning_profile = select({
|
||||
"//:ci_build": None,
|
||||
"//conditions:default": "Santa_Dev.provisionprofile",
|
||||
}),
|
||||
version = "//:version",
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [":santactl_lib"],
|
||||
)
|
||||
|
||||
@@ -117,46 +95,22 @@ santa_unit_test(
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTCommandSyncTest",
|
||||
srcs = [
|
||||
"Commands/sync/NSData+Zlib.h",
|
||||
"Commands/sync/NSData+Zlib.m",
|
||||
"Commands/sync/SNTCommandSync.m",
|
||||
"Commands/sync/SNTCommandSyncConstants.h",
|
||||
"Commands/sync/SNTCommandSyncConstants.m",
|
||||
"Commands/sync/SNTCommandSyncEventUpload.h",
|
||||
"Commands/sync/SNTCommandSyncEventUpload.m",
|
||||
"Commands/sync/SNTCommandSyncManager.h",
|
||||
"Commands/sync/SNTCommandSyncManager.m",
|
||||
"Commands/sync/SNTCommandSyncPostflight.h",
|
||||
"Commands/sync/SNTCommandSyncPostflight.m",
|
||||
"Commands/sync/SNTCommandSyncPreflight.h",
|
||||
"Commands/sync/SNTCommandSyncPreflight.m",
|
||||
"Commands/sync/SNTCommandSyncRuleDownload.h",
|
||||
"Commands/sync/SNTCommandSyncRuleDownload.m",
|
||||
"Commands/sync/SNTCommandSyncStage.h",
|
||||
"Commands/sync/SNTCommandSyncStage.m",
|
||||
"Commands/sync/SNTCommandSyncState.h",
|
||||
"Commands/sync/SNTCommandSyncState.m",
|
||||
"SNTCommand.h",
|
||||
"SNTCommand.m",
|
||||
"SNTCommandController.h",
|
||||
"SNTCommandController.m",
|
||||
],
|
||||
sdk_dylibs = ["libz"],
|
||||
name = "SNTCommandMetricsTest",
|
||||
srcs = ["Commands/SNTCommandMetricsTest.m"],
|
||||
structured_resources = glob(["Commands/testdata/*"]),
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [
|
||||
":FCM_lib",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDropRootPrivs",
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"//Source/common:SNTXPCSyncdInterface",
|
||||
"@MOLAuthenticatingURLSession",
|
||||
"@MOLXPCConnection",
|
||||
":santactl_lib",
|
||||
"//Source/santametricservice/Formats:SNTMetricFormatTestHelper",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":SNTCommandFileInfoTest",
|
||||
":SNTCommandMetricsTest",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
@@ -41,6 +41,7 @@ static NSString *const kCodeSigned = @"Code-signed";
|
||||
static NSString *const kRule = @"Rule";
|
||||
static NSString *const kSigningChain = @"Signing Chain";
|
||||
static NSString *const kUniversalSigningChain = @"Universal Signing Chain";
|
||||
static NSString *const kTeamID = @"Team ID";
|
||||
|
||||
// signing chain keys
|
||||
static NSString *const kCommonName = @"Common Name";
|
||||
@@ -70,7 +71,7 @@ NSString *formattedStringForKeyArray(NSArray<NSString *> *array) {
|
||||
// Properties set from commandline flags
|
||||
@property(nonatomic) BOOL recursive;
|
||||
@property(nonatomic) BOOL jsonOutput;
|
||||
@property(nonatomic) int certIndex; // 0 means no cert-index specified
|
||||
@property(nonatomic) NSNumber *certIndex;
|
||||
@property(nonatomic, copy) NSArray<NSString *> *outputKeyList;
|
||||
@property(nonatomic, copy) NSDictionary<NSString *, NSRegularExpression *> *outputFilters;
|
||||
|
||||
@@ -109,6 +110,7 @@ typedef id (^SNTAttributeBlock)(SNTCommandFileInfo *, SNTFileInfo *);
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadURL;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadTimestamp;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadAgent;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock teamID;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock type;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock pageZero;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock codeSigned;
|
||||
@@ -161,9 +163,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
@"%@\n"
|
||||
@" --cert-index: Supply an integer corresponding to a certificate of the\n"
|
||||
@" signing chain to show info only for that certificate.\n"
|
||||
@" 1 for the leaf certificate\n"
|
||||
@" -1 for the root certificate\n"
|
||||
@" 2 and up for the intermediates / root\n"
|
||||
@" 0 up to n for the leaf certificate up to the root\n"
|
||||
@" -1 down to -n-1 for the root certificate down to the leaf\n"
|
||||
@"\n"
|
||||
@" --filter: Use predicates of the form 'key=regex' to filter out which files\n"
|
||||
@" are displayed. Valid keys are the same as for --key. Value is a\n"
|
||||
@@ -183,7 +184,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
+ (NSArray<NSString *> *)fileInfoKeys {
|
||||
return @[
|
||||
kPath, kSHA256, kSHA1, kBundleName, kBundleVersion, kBundleVersionStr, kDownloadReferrerURL,
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kType, kPageZero, kCodeSigned, kRule,
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kType, kPageZero, kCodeSigned, kRule,
|
||||
kSigningChain, kUniversalSigningChain
|
||||
];
|
||||
}
|
||||
@@ -215,7 +216,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
kCodeSigned : self.codeSigned,
|
||||
kRule : self.rule,
|
||||
kSigningChain : self.signingChain,
|
||||
kUniversalSigningChain : self.universalSigningChain
|
||||
kUniversalSigningChain : self.universalSigningChain,
|
||||
kTeamID : self.teamID,
|
||||
};
|
||||
|
||||
_printQueue = dispatch_queue_create("com.google.santactl.print_queue", DISPATCH_QUEUE_SERIAL);
|
||||
@@ -355,13 +357,15 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
NSError *err;
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err];
|
||||
[[cmd.daemonConn remoteObjectProxy] decisionForFilePath:fileInfo.path
|
||||
fileSHA256:fileInfo.SHA256
|
||||
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
|
||||
reply:^(SNTEventState s) {
|
||||
state = s;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
[[cmd.daemonConn remoteObjectProxy]
|
||||
decisionForFilePath:fileInfo.path
|
||||
fileSHA256:fileInfo.SHA256
|
||||
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
|
||||
teamID:[csc.signingInformation valueForKey:@"teamid"]
|
||||
reply:^(SNTEventState s) {
|
||||
state = s;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
cmd.daemonUnavailable = YES;
|
||||
return kCommunicationErrorMsg;
|
||||
@@ -375,10 +379,13 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
case SNTEventStateBlockBinary: [output appendString:@" (Binary)"]; break;
|
||||
case SNTEventStateAllowCertificate:
|
||||
case SNTEventStateBlockCertificate: [output appendString:@" (Certificate)"]; break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
|
||||
case SNTEventStateAllowTransitive: [output appendString:@" (Transitive)"]; break;
|
||||
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
if (cmd.prettyOutput) {
|
||||
@@ -458,6 +465,13 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
};
|
||||
}
|
||||
|
||||
- (SNTAttributeBlock)teamID {
|
||||
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
|
||||
return [csc.signingInformation valueForKey:@"teamid"];
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
// Entry point for the command.
|
||||
@@ -575,12 +589,24 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
// First build up a dictionary containing all the information we want to print out
|
||||
NSMutableDictionary *outputDict = [NSMutableDictionary dictionary];
|
||||
if (self.certIndex) {
|
||||
int index = [self.certIndex intValue];
|
||||
|
||||
// --cert-index flag implicitly means that we want only the signing chain. So we find the
|
||||
// specified certificate in the signing chain, then print out values for all keys in cert.
|
||||
NSArray *signingChain = self.propertyMap[kSigningChain](self, fileInfo);
|
||||
if (!signingChain || !signingChain.count) return; // check signing chain isn't empty
|
||||
int index = (self.certIndex == -1) ? (int)signingChain.count - 1 : self.certIndex - 1;
|
||||
if (index < 0 || index >= (int)signingChain.count) return; // check that index is valid
|
||||
if (index < 0) {
|
||||
index = (int)signingChain.count - -(index);
|
||||
if (index < 0) {
|
||||
fprintf(stderr, "Invalid --cert-index: %d\n", index);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (index >= (int)signingChain.count) {
|
||||
fprintf(stderr, "Invalid --cert-index: %d\n", index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
NSDictionary *cert = signingChain[index];
|
||||
|
||||
// Check if we should skip over this item based on outputFilters.
|
||||
@@ -687,14 +713,12 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
}
|
||||
int index = 0;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:arguments[i]];
|
||||
if (![scanner scanInt:&index] || !scanner.atEnd || index == 0 || index < -1) {
|
||||
[self
|
||||
printErrorUsageAndExit:
|
||||
[NSString stringWithFormat:@"\n\"%@\" is an invalid argument for --cert-index\n"
|
||||
@" --cert-index argument must be one of -1, 1, 2, 3, ...",
|
||||
arguments[i]]];
|
||||
if (![scanner scanInt:&index] || !scanner.atEnd) {
|
||||
[self printErrorUsageAndExit:
|
||||
[NSString stringWithFormat:@"\n\"%@\" is an invalid argument for --cert-index\n",
|
||||
arguments[i]]];
|
||||
}
|
||||
self.certIndex = index;
|
||||
self.certIndex = @(index);
|
||||
} else if ([arg caseInsensitiveCompare:@"--key"] == NSOrderedSame) {
|
||||
i += 1; // advance to next argument and grab the key
|
||||
if (i >= nargs || [arguments[i] hasPrefix:@"--"]) {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
typedef id (^SNTAttributeBlock)(SNTCommandFileInfo *, SNTFileInfo *);
|
||||
@property(nonatomic) BOOL recursive;
|
||||
@property(nonatomic) BOOL jsonOutput;
|
||||
@property(nonatomic) int certIndex;
|
||||
@property(nonatomic) NSNumber *certIndex;
|
||||
@property(nonatomic, copy) NSArray<NSString *> *outputKeyList;
|
||||
@property(nonatomic) NSDictionary<NSString *, SNTAttributeBlock> *propertyMap;
|
||||
+ (NSArray *)fileInfoKeys;
|
||||
@@ -71,7 +71,7 @@ typedef id (^SNTAttributeBlock)(SNTCommandFileInfo *, SNTFileInfo *);
|
||||
|
||||
- (void)testParseArgumentsCertIndex {
|
||||
NSArray *filePaths = [self.cfi parseArguments:@[ @"--cert-index", @"1", @"/usr/bin/yes" ]];
|
||||
XCTAssertEqual(self.cfi.certIndex, 1);
|
||||
XCTAssertEqual([self.cfi.certIndex intValue], 1);
|
||||
XCTAssertTrue([filePaths containsObject:@"/usr/bin/yes"]);
|
||||
}
|
||||
|
||||
|
||||
22
Source/santactl/Commands/SNTCommandMetrics.h
Normal file
22
Source/santactl/Commands/SNTCommandMetrics.h
Normal file
@@ -0,0 +1,22 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
|
||||
@interface SNTCommandMetrics : SNTCommand <SNTCommandProtocol>
|
||||
- (void)prettyPrintMetrics:(NSDictionary *)metircs asJSON:(BOOL)exportJSON;
|
||||
@end
|
||||
145
Source/santactl/Commands/SNTCommandMetrics.m
Normal file
145
Source/santactl/Commands/SNTCommandMetrics.m
Normal file
@@ -0,0 +1,145 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/Commands/SNTCommandMetrics.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
|
||||
@implementation SNTCommandMetrics
|
||||
|
||||
REGISTER_COMMAND_NAME(@"metrics")
|
||||
|
||||
+ (BOOL)requiresRoot {
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)requiresDaemonConn {
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (NSString *)shortHelpText {
|
||||
return @"Show Santa metric information.";
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
return (@"Provides metrics about Santa's operation while it's running.\n"
|
||||
@" Use --json to output in JSON format");
|
||||
}
|
||||
|
||||
- (void)prettyPrintRootLabels:(NSDictionary *)rootLabels {
|
||||
for (NSString *label in rootLabels) {
|
||||
const char *labelStr = [label cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
const char *valueStr = [rootLabels[label] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
printf(" %-25s | %s\n", labelStr, valueStr);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)prettyPrintMetricValues:(NSDictionary *)metrics {
|
||||
for (NSString *metricName in metrics) {
|
||||
NSDictionary *metric = metrics[metricName];
|
||||
const char *metricNameStr = [metricName UTF8String];
|
||||
const char *description = [metric[@"description"] UTF8String];
|
||||
NSString *metricType = SNTMetricMakeStringFromMetricType([metric[@"type"] integerValue]);
|
||||
const char *metricTypeStr = [metricType UTF8String];
|
||||
|
||||
printf(" %-25s | %s\n", "Metric Name", metricNameStr);
|
||||
printf(" %-25s | %s\n", "Description", description);
|
||||
printf(" %-25s | %s\n", "Type", metricTypeStr);
|
||||
|
||||
for (NSString *fieldName in metric[@"fields"]) {
|
||||
for (NSDictionary *field in metric[@"fields"][fieldName]) {
|
||||
const char *fieldNameStr = [fieldName cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
const char *fieldValueStr = [field[@"value"] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
const char *createdStr = [field[@"created"] UTF8String];
|
||||
const char *lastUpdatedStr = [field[@"last_updated"] UTF8String];
|
||||
const char *data = [[NSString stringWithFormat:@"%@", field[@"data"]] UTF8String];
|
||||
|
||||
if (strlen(fieldNameStr) > 0) {
|
||||
printf(" %-25s | %s=%s\n", "Field", fieldNameStr, fieldValueStr);
|
||||
}
|
||||
|
||||
printf(" %-25s | %s\n", "Created", createdStr);
|
||||
printf(" %-25s | %s\n", "Last Updated", lastUpdatedStr);
|
||||
printf(" %-25s | %s\n", "Data", data);
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)prettyPrintMetrics:(NSDictionary *)metrics asJSON:(BOOL)exportJSON {
|
||||
BOOL exportMetrics = [[SNTConfigurator configurator] exportMetrics];
|
||||
NSURL *metricsURLStr = [[SNTConfigurator configurator] metricURL];
|
||||
SNTMetricFormatType metricFormat = [[SNTConfigurator configurator] metricFormat];
|
||||
NSUInteger metricExportInterval = [[SNTConfigurator configurator] metricExportInterval];
|
||||
NSDictionary *normalizedMetrics = SNTMetricConvertDatesToISO8601Strings(metrics);
|
||||
|
||||
if (exportJSON) {
|
||||
// Format
|
||||
NSData *metricData = [NSJSONSerialization dataWithJSONObject:normalizedMetrics
|
||||
options:NSJSONWritingPrettyPrinted
|
||||
error:nil];
|
||||
NSString *metricStr = [[NSString alloc] initWithData:metricData encoding:NSUTF8StringEncoding];
|
||||
printf("%s\n", [metricStr UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!exportMetrics) {
|
||||
printf("Metrics not configured\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf(">>> Metrics Info\n");
|
||||
printf(" %-25s | %s\n", "Metrics Server", [metricsURLStr.absoluteString UTF8String]);
|
||||
printf(" %-25s | %s\n", "Metrics Format",
|
||||
[SNTMetricStringFromMetricFormatType(metricFormat) UTF8String]);
|
||||
printf(" %-25s | %lu\n", "Export Interval (seconds)", metricExportInterval);
|
||||
printf("\n");
|
||||
|
||||
printf(">>> Root Labels\n");
|
||||
[self prettyPrintRootLabels:normalizedMetrics[@"root_labels"]];
|
||||
printf("\n");
|
||||
printf(">>> Metrics \n");
|
||||
[self prettyPrintMetricValues:normalizedMetrics[@"metrics"]];
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
__block NSDictionary *metrics;
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
|
||||
[[self.daemonConn remoteObjectProxy] metrics:^(NSDictionary *exportedMetrics) {
|
||||
metrics = exportedMetrics;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Wait a maximum of 5s for metrics collected from daemon to arrive.
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
|
||||
fprintf(stderr, "Failed to retrieve metrics from daemon\n\n");
|
||||
}
|
||||
|
||||
[self prettyPrintMetrics:metrics asJSON:[arguments containsObject:@"--json"]];
|
||||
exit(0);
|
||||
}
|
||||
|
||||
@end
|
||||
134
Source/santactl/Commands/SNTCommandMetricsTest.m
Normal file
134
Source/santactl/Commands/SNTCommandMetricsTest.m
Normal file
@@ -0,0 +1,134 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/santactl/Commands/SNTCommandMetrics.h"
|
||||
#import "Source/santametricservice/Formats/SNTMetricFormatTestHelper.h"
|
||||
|
||||
@interface SNTCommandMetricsTest : XCTestCase
|
||||
@property NSString *tempDir;
|
||||
@property id mockConfigurator;
|
||||
@end
|
||||
|
||||
@implementation SNTCommandMetricsTest
|
||||
|
||||
- (void)setUp {
|
||||
// create a temp dir
|
||||
char template[] = "/tmp/sntcommandmetrictest.XXXXXXX";
|
||||
char *tempPath = mkdtemp(template);
|
||||
|
||||
if (tempPath == NULL) {
|
||||
NSLog(@"Unable to make temp directory");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
self.tempDir =
|
||||
[[NSFileManager defaultManager] stringWithFileSystemRepresentation:tempPath
|
||||
length:strlen(tempPath)];
|
||||
// mock the SNTConfigurator
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator exportMetrics]).andReturn(YES);
|
||||
OCMStub([self.mockConfigurator metricFormat]).andReturn(SNTMetricFormatTypeMonarchJSON);
|
||||
OCMStub([self.mockConfigurator metricURL])
|
||||
.andReturn([NSURL URLWithString:@"http://localhost:2444/submit"]);
|
||||
OCMStub([self.mockConfigurator metricExportInterval]).andReturn((NSUInteger)30);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
// delete the temp dir
|
||||
NSError *err;
|
||||
[[NSFileManager defaultManager] removeItemAtPath:self.tempDir error:&err];
|
||||
|
||||
if (err != nil) {
|
||||
NSLog(@"unable to remove %@, error: %@", self.tempDir, err);
|
||||
}
|
||||
|
||||
dup2(1, STDOUT_FILENO);
|
||||
}
|
||||
|
||||
- (void)testPrettyPrintingJSON {
|
||||
NSError *err;
|
||||
NSString *path = [[NSBundle bundleForClass:[self class]] resourcePath];
|
||||
path = [path stringByAppendingPathComponent:@"Commands/testdata/metrics-prettyprint.json"];
|
||||
|
||||
NSString *goldenFileContents = [[NSString alloc]
|
||||
initWithData:[NSData dataWithContentsOfFile:path options:NSDataReadingUncached error:&err]
|
||||
encoding:NSUTF8StringEncoding];
|
||||
|
||||
XCTAssertNil(err, @"failed to read golden file %@ for testPrettyPrintingJSON", path);
|
||||
|
||||
SNTCommandMetrics *metricsCmd = [[SNTCommandMetrics alloc] init];
|
||||
|
||||
NSString *outputPath = [NSString pathWithComponents:@[ self.tempDir, @"test.data" ]];
|
||||
|
||||
// redirect stdout
|
||||
int fd = open([outputPath UTF8String], O_TRUNC | O_WRONLY | O_CREAT, 0600);
|
||||
int saved_stdout = dup(fileno(stdout));
|
||||
dup2(fd, fileno(stdout));
|
||||
|
||||
[metricsCmd prettyPrintMetrics:[SNTMetricFormatTestHelper createValidMetricsDictionary]
|
||||
asJSON:YES];
|
||||
|
||||
// restore stdout
|
||||
fflush(stdout);
|
||||
dup2(saved_stdout, fileno(stdout));
|
||||
|
||||
// open test file assert equal with golden file
|
||||
NSString *commandOutput =
|
||||
[[NSString alloc] initWithData:[NSData dataWithContentsOfFile:outputPath]
|
||||
encoding:NSUTF8StringEncoding];
|
||||
XCTAssertEqualObjects(goldenFileContents, commandOutput,
|
||||
@"Metrics command command did not produce expected output");
|
||||
}
|
||||
|
||||
- (void)testPrettyPrinting {
|
||||
NSError *err;
|
||||
NSString *path = [[NSBundle bundleForClass:[self class]] resourcePath];
|
||||
path = [path stringByAppendingPathComponent:@"Commands/testdata/metrics-prettyprint.txt"];
|
||||
|
||||
NSString *goldenFileContents = [[NSString alloc]
|
||||
initWithData:[NSData dataWithContentsOfFile:path options:NSDataReadingUncached error:&err]
|
||||
encoding:NSUTF8StringEncoding];
|
||||
|
||||
XCTAssertNil(err, @"failed to read golden file %@ for testPrettyPrinting", path);
|
||||
|
||||
SNTCommandMetrics *metricsCmd = [[SNTCommandMetrics alloc] init];
|
||||
|
||||
NSString *outputPath = [NSString pathWithComponents:@[ self.tempDir, @"test.data" ]];
|
||||
|
||||
// redirect stdout
|
||||
int fd = open([outputPath UTF8String], O_TRUNC | O_WRONLY | O_CREAT, 0600);
|
||||
int saved_stdout = dup(fileno(stdout));
|
||||
dup2(fd, fileno(stdout));
|
||||
|
||||
[metricsCmd prettyPrintMetrics:[SNTMetricFormatTestHelper createValidMetricsDictionary]
|
||||
asJSON:NO];
|
||||
|
||||
// restore stdout
|
||||
fflush(stdout);
|
||||
dup2(saved_stdout, fileno(stdout));
|
||||
|
||||
// open test file assert equal with golden file
|
||||
NSString *commandOutput =
|
||||
[[NSString alloc] initWithData:[NSData dataWithContentsOfFile:outputPath]
|
||||
encoding:NSUTF8StringEncoding];
|
||||
XCTAssertEqualObjects(goldenFileContents, commandOutput,
|
||||
@"Metrics command command did not produce expected output");
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -60,9 +60,11 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@" Will add the hash of the file currently at that path.\n"
|
||||
@" Does not work with --check. Use the fileinfo verb to check.\n"
|
||||
@" the rule state of a file.\n"
|
||||
@" --sha256 {sha256}: hash to add/remove/check\n"
|
||||
@" --identifier {sha256|teamID}: identifier to add/remove/check\n"
|
||||
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
|
||||
@"\n"
|
||||
@" Optionally:\n"
|
||||
@" --teamid: add or check a team ID rule instead of binary\n"
|
||||
@" --certificate: add or check a certificate sha256 rule instead of binary\n"
|
||||
#ifdef DEBUG
|
||||
@" --force: allow manual changes even when SyncBaseUrl is set\n"
|
||||
@@ -111,17 +113,24 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
check = YES;
|
||||
} else if ([arg caseInsensitiveCompare:@"--certificate"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeCertificate;
|
||||
} else if ([arg caseInsensitiveCompare:@"--teamid"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeTeamID;
|
||||
} else if ([arg caseInsensitiveCompare:@"--path"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--path requires an argument"];
|
||||
}
|
||||
path = arguments[i];
|
||||
} else if ([arg caseInsensitiveCompare:@"--identifier"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--identifier requires an argument"];
|
||||
}
|
||||
newRule.identifier = arguments[i];
|
||||
} else if ([arg caseInsensitiveCompare:@"--sha256"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--sha256 requires an argument"];
|
||||
}
|
||||
newRule.shasum = arguments[i];
|
||||
if (newRule.shasum.length != 64) {
|
||||
newRule.identifier = arguments[i];
|
||||
if (newRule.identifier.length != 64) {
|
||||
[self printErrorUsageAndExit:@"--sha256 requires a valid SHA-256 as the argument"];
|
||||
}
|
||||
} else if ([arg caseInsensitiveCompare:@"--message"] == NSOrderedSame) {
|
||||
@@ -139,7 +148,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
|
||||
if (check) {
|
||||
if (!newRule.shasum) return [self printErrorUsageAndExit:@"--check requires --sha256"];
|
||||
if (!newRule.identifier) return [self printErrorUsageAndExit:@"--check requires --identifier"];
|
||||
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
|
||||
}
|
||||
|
||||
@@ -150,17 +159,18 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
|
||||
if (newRule.type == SNTRuleTypeBinary) {
|
||||
newRule.shasum = fi.SHA256;
|
||||
newRule.identifier = fi.SHA256;
|
||||
} else if (newRule.type == SNTRuleTypeCertificate) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.shasum = cs.leafCertificate.SHA256;
|
||||
newRule.identifier = cs.leafCertificate.SHA256;
|
||||
} else if (newRule.type == SNTRuleTypeTeamID) {
|
||||
}
|
||||
}
|
||||
|
||||
if (newRule.state == SNTRuleStateUnknown) {
|
||||
[self printErrorUsageAndExit:@"No state specified"];
|
||||
} else if (!newRule.shasum) {
|
||||
[self printErrorUsageAndExit:@"Either SHA-256 or path to file must be specified"];
|
||||
} else if (!newRule.identifier) {
|
||||
[self printErrorUsageAndExit:@"Either SHA-256, team ID, or path to file must be specified"];
|
||||
}
|
||||
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
@@ -173,10 +183,25 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
LOGD(@"Failure reason: %@", error.localizedFailureReason);
|
||||
exit(1);
|
||||
} else {
|
||||
NSString *ruleType;
|
||||
switch (newRule.type) {
|
||||
case SNTRuleTypeCertificate:
|
||||
case SNTRuleTypeBinary: {
|
||||
ruleType = @"SHA-256";
|
||||
break;
|
||||
}
|
||||
case SNTRuleTypeTeamID: {
|
||||
ruleType = @"Team ID";
|
||||
break;
|
||||
}
|
||||
default: ruleType = @"(Unknown type)";
|
||||
}
|
||||
if (newRule.state == SNTRuleStateRemove) {
|
||||
printf("Removed rule for SHA-256: %s.\n", [newRule.shasum UTF8String]);
|
||||
printf("Removed rule for %s: %s.\n", [ruleType UTF8String],
|
||||
[newRule.identifier UTF8String]);
|
||||
} else {
|
||||
printf("Added rule for SHA-256: %s.\n", [newRule.shasum UTF8String]);
|
||||
printf("Added rule for %s: %s.\n", [ruleType UTF8String],
|
||||
[newRule.identifier UTF8String]);
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
@@ -184,14 +209,16 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
|
||||
- (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)daemonConn {
|
||||
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.shasum : nil;
|
||||
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.shasum : nil;
|
||||
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil;
|
||||
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil;
|
||||
NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil;
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
__block NSMutableString *output;
|
||||
[[daemonConn remoteObjectProxy] decisionForFilePath:nil
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
reply:^(SNTEventState s) {
|
||||
output = (SNTEventStateAllow & s)
|
||||
? @"Allowed".mutableCopy
|
||||
@@ -219,6 +246,10 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
case SNTEventStateAllowTransitive:
|
||||
[output appendString:@" (Transitive)"];
|
||||
break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID:
|
||||
[output appendString:@" (TeamID)"];
|
||||
break;
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
@@ -244,6 +275,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
[[daemonConn remoteObjectProxy]
|
||||
databaseRuleForBinarySHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
reply:^(SNTRule *r) {
|
||||
if (r.state == SNTRuleStateAllowTransitive) {
|
||||
NSDate *date =
|
||||
|
||||
@@ -94,17 +94,19 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}
|
||||
|
||||
// Database counts
|
||||
__block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1;
|
||||
__block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1, teamIDRuleCount = -1;
|
||||
__block int64_t compilerRuleCount = -1, transitiveRuleCount = -1;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] databaseRuleCounts:^(int64_t binary, int64_t certificate,
|
||||
int64_t compiler, int64_t transitive) {
|
||||
binaryRuleCount = binary;
|
||||
certRuleCount = certificate;
|
||||
compilerRuleCount = compiler;
|
||||
transitiveRuleCount = transitive;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler, int64_t transitive,
|
||||
int64_t teamID) {
|
||||
binaryRuleCount = binary;
|
||||
certRuleCount = certificate;
|
||||
teamIDRuleCount = teamID;
|
||||
compilerRuleCount = compiler;
|
||||
transitiveRuleCount = transitive;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] databaseEventCount:^(int64_t count) {
|
||||
eventCount = count;
|
||||
@@ -172,6 +174,10 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
|
||||
NSString *syncURLStr = [[[SNTConfigurator configurator] syncBaseURL] absoluteString];
|
||||
|
||||
BOOL exportMetrics = [[SNTConfigurator configurator] exportMetrics];
|
||||
NSURL *metricsURLStr = [[SNTConfigurator configurator] metricURL];
|
||||
NSUInteger metricExportInterval = [[SNTConfigurator configurator] metricExportInterval];
|
||||
|
||||
if ([arguments containsObject:@"--json"]) {
|
||||
NSMutableDictionary *stats = [@{
|
||||
@"daemon" : @{
|
||||
@@ -226,6 +232,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(">>> Database Info\n");
|
||||
printf(" %-25s | %lld\n", "Binary Rules", binaryRuleCount);
|
||||
printf(" %-25s | %lld\n", "Certificate Rules", certRuleCount);
|
||||
printf(" %-25s | %lld\n", "TeamID Rules", teamIDRuleCount);
|
||||
printf(" %-25s | %lld\n", "Compiler Rules", compilerRuleCount);
|
||||
printf(" %-25s | %lld\n", "Transitive Rules", transitiveRuleCount);
|
||||
printf(" %-25s | %lld\n", "Events Pending Upload", eventCount);
|
||||
@@ -241,6 +248,12 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %s\n", "Bundle Scanning", (enableBundles ? "Yes" : "No"));
|
||||
printf(" %-25s | %s\n", "Transitive Rules", (enableTransitiveRules ? "Yes" : "No"));
|
||||
}
|
||||
|
||||
if (exportMetrics) {
|
||||
printf(">>> Metrics Info\n");
|
||||
printf(" %-25s | %s\n", "Metrics Server", [[metricsURLStr absoluteString] UTF8String]);
|
||||
printf(" %-25s | %lu\n", "Export Interval (seconds)", metricExportInterval);
|
||||
}
|
||||
}
|
||||
|
||||
exit(0);
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
#import "Source/common/SNTDropRootPrivs.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/Commands/sync/SNTCommandSyncManager.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
#import "Source/santasyncservice/SNTSyncManager.h"
|
||||
|
||||
@interface SNTCommandSync : SNTCommand <SNTCommandProtocol>
|
||||
@property MOLXPCConnection *listener;
|
||||
@property SNTCommandSyncManager *syncManager;
|
||||
@property SNTSyncManager *syncManager;
|
||||
@end
|
||||
|
||||
@implementation SNTCommandSync
|
||||
@@ -68,8 +68,8 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
}
|
||||
|
||||
BOOL daemon = [arguments containsObject:@"--daemon"];
|
||||
self.syncManager = [[SNTCommandSyncManager alloc] initWithDaemonConnection:self.daemonConn
|
||||
isDaemon:daemon];
|
||||
self.syncManager = [[SNTSyncManager alloc] initWithDaemonConnection:self.daemonConn
|
||||
isDaemon:daemon];
|
||||
|
||||
// Dropping root privileges to the 'nobody' user causes the default NSURLCache to throw
|
||||
// sandbox errors, which are benign but annoying. This line disables the cache entirely.
|
||||
@@ -1 +0,0 @@
|
||||
{"rules": [{"rule_type": "CERTIFICATE", "policy": "BLACKLIST", "sha256": "7846698e47ef41be80b83fb9e2b98fa6dc46c9188b068bff323c302955a00142", "custom_msg": "Hi There"}]}
|
||||
118
Source/santactl/Commands/testdata/metrics-prettyprint.json
vendored
Normal file
118
Source/santactl/Commands/testdata/metrics-prettyprint.json
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"metrics" : {
|
||||
"\/santa\/rules" : {
|
||||
"type" : 7,
|
||||
"description" : "Number of rules",
|
||||
"fields" : {
|
||||
"rule_type" : [
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "binary",
|
||||
"data" : 1
|
||||
},
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "certificate",
|
||||
"data" : 3
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/proc\/memory\/resident_size" : {
|
||||
"type" : 7,
|
||||
"description" : "The resident set size of this process",
|
||||
"fields" : {
|
||||
"" : [
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "",
|
||||
"data" : 123456789
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/santa\/events" : {
|
||||
"type" : 9,
|
||||
"description" : "Count of process exec events on the host",
|
||||
"fields" : {
|
||||
"rule_type" : [
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "binary",
|
||||
"data" : 1
|
||||
},
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "certificate",
|
||||
"data" : 2
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/santa\/using_endpoint_security_framework" : {
|
||||
"type" : 1,
|
||||
"description" : "Is santad using the endpoint security framework",
|
||||
"fields" : {
|
||||
"" : [
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "",
|
||||
"data" : true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/proc\/birth_timestamp" : {
|
||||
"type" : 3,
|
||||
"description" : "Start time of this santad instance, in microseconds since epoch",
|
||||
"fields" : {
|
||||
"" : [
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "",
|
||||
"data" : 1250999830800
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/proc\/memory\/virtual_size" : {
|
||||
"type" : 7,
|
||||
"description" : "The virtual memory size of this process",
|
||||
"fields" : {
|
||||
"" : [
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "",
|
||||
"data" : 987654321
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/build\/label" : {
|
||||
"type" : 2,
|
||||
"description" : "Software version running",
|
||||
"fields" : {
|
||||
"" : [
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "",
|
||||
"data" : "20210809.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"root_labels" : {
|
||||
"hostname" : "testHost",
|
||||
"username" : "testUser"
|
||||
}
|
||||
}
|
||||
69
Source/santactl/Commands/testdata/metrics-prettyprint.txt
vendored
Normal file
69
Source/santactl/Commands/testdata/metrics-prettyprint.txt
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
>>> Metrics Info
|
||||
Metrics Server | http://localhost:2444/submit
|
||||
Metrics Format | monarchjson
|
||||
Export Interval (seconds) | 30
|
||||
|
||||
>>> Root Labels
|
||||
hostname | testHost
|
||||
username | testUser
|
||||
|
||||
>>> Metrics
|
||||
Metric Name | /santa/rules
|
||||
Description | Number of rules
|
||||
Type | SNTMetricTypeGaugeInt64 7
|
||||
Field | rule_type=binary
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 1
|
||||
Field | rule_type=certificate
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 3
|
||||
|
||||
Metric Name | /proc/memory/resident_size
|
||||
Description | The resident set size of this process
|
||||
Type | SNTMetricTypeGaugeInt64 7
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 123456789
|
||||
|
||||
Metric Name | /santa/events
|
||||
Description | Count of process exec events on the host
|
||||
Type | SNTMetricTypeCounter 9
|
||||
Field | rule_type=binary
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 1
|
||||
Field | rule_type=certificate
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 2
|
||||
|
||||
Metric Name | /santa/using_endpoint_security_framework
|
||||
Description | Is santad using the endpoint security framework
|
||||
Type | SNTMetricTypeConstantBool 1
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 1
|
||||
|
||||
Metric Name | /proc/birth_timestamp
|
||||
Description | Start time of this santad instance, in microseconds since epoch
|
||||
Type | SNTMetricTypeConstantInt64 3
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 1250999830800
|
||||
|
||||
Metric Name | /proc/memory/virtual_size
|
||||
Description | The virtual memory size of this process
|
||||
Type | SNTMetricTypeGaugeInt64 7
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 987654321
|
||||
|
||||
Metric Name | /build/label
|
||||
Description | Software version running
|
||||
Type | SNTMetricTypeConstantString 2
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 20210809.0.1
|
||||
|
||||
@@ -12,12 +12,14 @@ objc_library(
|
||||
"DataLayer/SNTEventTable.m",
|
||||
"DataLayer/SNTRuleTable.h",
|
||||
"DataLayer/SNTRuleTable.m",
|
||||
"EventProviders/SNTCachingEndpointSecurityManager.h",
|
||||
"EventProviders/SNTCachingEndpointSecurityManager.mm",
|
||||
"EventProviders/SNTDeviceManager.h",
|
||||
"EventProviders/SNTDeviceManager.mm",
|
||||
"EventProviders/SNTDriverManager.h",
|
||||
"EventProviders/SNTDriverManager.m",
|
||||
"EventProviders/SNTEndpointSecurityManager.h",
|
||||
"EventProviders/SNTEndpointSecurityManager.mm",
|
||||
"EventProviders/SNTCachingEndpointSecurityManager.h",
|
||||
"EventProviders/SNTCachingEndpointSecurityManager.mm",
|
||||
"EventProviders/SNTEventProvider.h",
|
||||
"Logs/SNTEventLog.h",
|
||||
"Logs/SNTEventLog.m",
|
||||
@@ -52,7 +54,6 @@ objc_library(
|
||||
"IOKit",
|
||||
],
|
||||
deps = [
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SNTBlockMessage",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
@@ -61,43 +62,80 @@ objc_library(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTKernelCommon",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTPrefixTree",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"//Source/common:SNTXPCMetricServiceInterface",
|
||||
"//Source/common:SNTXPCNotifierInterface",
|
||||
"//Source/common:SNTXPCSyncdInterface",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/santad:SNTApplicationCoreMetrics",
|
||||
"@FMDB",
|
||||
"@MOLCodesignChecker",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTApplicationCoreMetrics",
|
||||
srcs = ["SNTApplicationCoreMetrics.m"],
|
||||
hdrs = ["SNTApplicationCoreMetrics.h"],
|
||||
deps = [
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTSystemInfo",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "EndpointSecurityTestLib",
|
||||
testonly = 1,
|
||||
srcs = [
|
||||
"EventProviders/EndpointSecurityTestUtil.h",
|
||||
"EventProviders/EndpointSecurityTestUtil.mm",
|
||||
],
|
||||
testonly = 1,
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
"bsm",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"DiskArbitration",
|
||||
"IOKit",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "DiskArbitrationTestLib",
|
||||
testonly = 1,
|
||||
srcs = [
|
||||
"EventProviders/DiskArbitrationTestUtil.h",
|
||||
"EventProviders/DiskArbitrationTestUtil.mm",
|
||||
],
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
"bsm",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"DiskArbitration",
|
||||
"IOKit",
|
||||
],
|
||||
)
|
||||
|
||||
macos_bundle(
|
||||
name = "com.google.santa.daemon",
|
||||
bundle_extension = "systemextension",
|
||||
bundle_id = "com.google.santa.daemon",
|
||||
infoplists = ["Info.plist"],
|
||||
linkopts = ["-execute"],
|
||||
minimum_os_version = "10.9",
|
||||
codesignopts = [
|
||||
"--timestamp",
|
||||
"--force",
|
||||
"--options library,kill,runtime",
|
||||
],
|
||||
infoplists = ["Info.plist"],
|
||||
linkopts = ["-execute"],
|
||||
minimum_os_version = "10.9",
|
||||
provisioning_profile = select({
|
||||
"//:ci_build": None,
|
||||
"//conditions:default": "Santa_Daemon_Dev.provisionprofile",
|
||||
@@ -110,37 +148,14 @@ macos_bundle(
|
||||
santa_unit_test(
|
||||
name = "SNTExecutionControllerTest",
|
||||
srcs = [
|
||||
"DataLayer/SNTDatabaseTable.h",
|
||||
"DataLayer/SNTDatabaseTable.m",
|
||||
"DataLayer/SNTEventTable.h",
|
||||
"DataLayer/SNTEventTable.m",
|
||||
"DataLayer/SNTRuleTable.h",
|
||||
"DataLayer/SNTRuleTable.m",
|
||||
"EventProviders/SNTDriverManager.h",
|
||||
"EventProviders/SNTDriverManager.m",
|
||||
"EventProviders/SNTEndpointSecurityManager.h",
|
||||
"EventProviders/SNTEndpointSecurityManager.mm",
|
||||
"EventProviders/SNTEventProvider.h",
|
||||
"Logs/SNTEventLog.h",
|
||||
"Logs/SNTEventLog.m",
|
||||
"SNTDatabaseController.h",
|
||||
"SNTDatabaseController.m",
|
||||
"SNTExecutionController.h",
|
||||
"SNTExecutionController.m",
|
||||
"SNTExecutionControllerTest.m",
|
||||
"SNTNotificationQueue.h",
|
||||
"SNTNotificationQueue.m",
|
||||
"SNTPolicyProcessor.h",
|
||||
"SNTPolicyProcessor.m",
|
||||
"SNTSyncdQueue.h",
|
||||
"SNTSyncdQueue.m",
|
||||
],
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
"//Source/common:SantaCache",
|
||||
":santad_lib",
|
||||
"//Source/common:SNTBlockMessage",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
@@ -148,10 +163,12 @@ santa_unit_test(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTKernelCommon",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTPrefixTree",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTXPCNotifierInterface",
|
||||
"//Source/common:SNTXPCSyncdInterface",
|
||||
"//Source/common:SantaCache",
|
||||
"@MOLCodesignChecker",
|
||||
"@MOLXPCConnection",
|
||||
"@OCMock",
|
||||
@@ -201,10 +218,15 @@ santa_unit_test(
|
||||
santa_unit_test(
|
||||
name = "SNTEndpointSecurityManagerTest",
|
||||
srcs = [
|
||||
"EventProviders/SNTEndpointSecurityManager.h",
|
||||
"EventProviders/SNTEndpointSecurityManager.mm",
|
||||
"EventProviders/SNTEndpointSecurityManagerTest.mm",
|
||||
"EventProviders/SNTEventProvider.h",
|
||||
"EventProviders/SNTEndpointSecurityManager.mm",
|
||||
"EventProviders/SNTEndpointSecurityManager.h",
|
||||
],
|
||||
minimum_os_version = "10.15",
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityTestLib",
|
||||
@@ -212,31 +234,98 @@ santa_unit_test(
|
||||
"//Source/common:SNTPrefixTree",
|
||||
"//Source/common:SantaCache",
|
||||
],
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
"bsm",
|
||||
],
|
||||
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTApplicationTest",
|
||||
data = [
|
||||
"//Source/santad/testdata:binaryrules_testdata",
|
||||
],
|
||||
name = "SNTDeviceManagerTest",
|
||||
srcs = [
|
||||
"SNTApplicationTest.m"
|
||||
],
|
||||
deps = [
|
||||
":santad_lib",
|
||||
":EndpointSecurityTestLib",
|
||||
"@MOLCodesignChecker",
|
||||
"@MOLXPCConnection",
|
||||
"@OCMock",
|
||||
"EventProviders/SNTDeviceManagerTest.mm",
|
||||
],
|
||||
minimum_os_version = "10.15",
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":DiskArbitrationTestLib",
|
||||
":EndpointSecurityTestLib",
|
||||
":santad_lib",
|
||||
"//Source/common:SNTKernelCommon",
|
||||
"//Source/common:SNTPrefixTree",
|
||||
"//Source/common:SantaCache",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTApplicationTest",
|
||||
srcs = [
|
||||
"SNTApplicationTest.m",
|
||||
],
|
||||
data = [
|
||||
"//Source/santad/testdata:binaryrules_testdata",
|
||||
],
|
||||
minimum_os_version = "10.15",
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityTestLib",
|
||||
":santad_lib",
|
||||
"@MOLCodesignChecker",
|
||||
"@MOLXPCConnection",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTApplicationBenchmark",
|
||||
srcs = [
|
||||
"SNTApplicationBenchmark.m",
|
||||
],
|
||||
data = [
|
||||
"//Source/santad/testdata:binaryrules_testdata",
|
||||
],
|
||||
minimum_os_version = "10.15",
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityTestLib",
|
||||
":santad_lib",
|
||||
"@MOLCodesignChecker",
|
||||
"@MOLXPCConnection",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTApplicationCoreMetricsTest",
|
||||
srcs = [
|
||||
"SNTApplicationCoreMetricsTest.m",
|
||||
],
|
||||
minimum_os_version = "10.15",
|
||||
deps = [
|
||||
":SNTApplicationCoreMetrics",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTSystemInfo",
|
||||
"//Source/santametricservice/Formats:SNTMetricFormatTestHelper",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":SNTApplicationCoreMetricsTest",
|
||||
":SNTApplicationTest",
|
||||
":SNTEndpointSecurityManagerTest",
|
||||
":SNTEventTableTest",
|
||||
":SNTExecutionControllerTest",
|
||||
":SNTRuleTableTest",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
@@ -51,12 +51,18 @@
|
||||
///
|
||||
- (NSUInteger)certificateRuleCount;
|
||||
|
||||
///
|
||||
/// @return Number of team ID rules in the database
|
||||
///
|
||||
- (NSUInteger)teamIDRuleCount;
|
||||
|
||||
///
|
||||
/// @return Rule for binary or certificate with given SHA-256. The binary rule will be returned
|
||||
/// if it exists. If not, the certificate rule will be returned if it exists.
|
||||
///
|
||||
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256;
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID;
|
||||
|
||||
///
|
||||
/// Add an array of rules to the database. The rules will be added within a transaction and the
|
||||
|
||||
@@ -39,11 +39,25 @@ static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
|
||||
@implementation SNTRuleTable
|
||||
|
||||
// ES on Monterey now has a “default mute set” of paths that are automatically applied to each ES
|
||||
// client. This mute set contains most (not all) AUTH event types for some paths that were deemed
|
||||
// “system critical”.
|
||||
// Retain this list for < 12.0 versions of ES, but we should be able to rely on the paths muted by
|
||||
// default (visible with es_muted_paths_events any time after connecting a new client and before
|
||||
// modifying any of the mute state).
|
||||
+ (NSArray *)criticalSystemBinaryPaths {
|
||||
return @[
|
||||
@"/usr/libexec/trustd",
|
||||
@"/usr/sbin/securityd",
|
||||
@"/usr/libexec/xpcproxy",
|
||||
@"/usr/libexec/amfid",
|
||||
@"/usr/libexec/opendirectoryd",
|
||||
@"/usr/libexec/runningboardd",
|
||||
@"/usr/libexec/syspolicyd",
|
||||
@"/usr/libexec/watchdogd",
|
||||
@"/usr/libexec/cfprefsd",
|
||||
@"/usr/sbin/securityd",
|
||||
@"/System/Library/PrivateFrameworks/TCC.framework/Versions/A/Resources/tccd",
|
||||
@"/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/Resources/WindowServer",
|
||||
@"/usr/sbin/ocspd",
|
||||
@"/usr/lib/dyld",
|
||||
@"/Applications/Santa.app/Contents/MacOS/Santa",
|
||||
@@ -120,6 +134,11 @@ static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
[db executeUpdate:@"ALTER TABLE 'rules' ADD 'timestamp' INTEGER"];
|
||||
newVersion = 3;
|
||||
}
|
||||
if (version < 4) {
|
||||
// Rename `shasum` column to `identifier`.
|
||||
[db executeUpdate:@"ALTER TABLE 'rules' RENAME COLUMN 'shasum' TO 'identifier'"];
|
||||
newVersion = 4;
|
||||
}
|
||||
|
||||
// Save signing info for launchd and santad. Used to ensure they are always allowed.
|
||||
self.santadCSInfo = [[MOLCodesignChecker alloc] initWithSelf];
|
||||
@@ -175,25 +194,36 @@ static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)teamIDRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=3"];
|
||||
}];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs {
|
||||
return [[SNTRule alloc] initWithShasum:[rs stringForColumn:@"shasum"]
|
||||
state:[rs intForColumn:@"state"]
|
||||
type:[rs intForColumn:@"type"]
|
||||
customMsg:[rs stringForColumn:@"custommsg"]
|
||||
timestamp:[rs intForColumn:@"timestamp"]];
|
||||
return [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
|
||||
state:[rs intForColumn:@"state"]
|
||||
type:[rs intForColumn:@"type"]
|
||||
customMsg:[rs stringForColumn:@"custommsg"]
|
||||
timestamp:[rs intForColumn:@"timestamp"]];
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256 {
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID {
|
||||
__block SNTRule *rule;
|
||||
|
||||
// NOTE: This code is written with the intention that the binary rule is searched for first
|
||||
// as Santa is designed to go with the most-specific rule possible. As such the query
|
||||
// should have "ORDER BY type DESC" before the LIMIT, to ensure that is the case. However,
|
||||
// in all tested versions of SQLite that ORDER BY clause is unnecessary: the query is
|
||||
// performed 'as written' by doing 2 separate lookups in the index and the second is skipped
|
||||
// if the first returns a result. That behavior can be checked here:
|
||||
// http://sqlfiddle.com/#!5/09c8a/2
|
||||
// as Santa is designed to go with the most-specific rule possible.
|
||||
//
|
||||
// The intended order of precedence is Binaries > Certificates > Team IDs.
|
||||
//
|
||||
// As such the query should have "ORDER BY type DESC" before the LIMIT, to ensure that is the
|
||||
// case. However, in all tested versions of SQLite that ORDER BY clause is unnecessary: the query
|
||||
// is performed 'as written' by doing separate lookups in the index and the later lookups are if
|
||||
// the first returns a result. That behavior can be checked here: http://sqlfiddle.com/#!5/cdc42/1
|
||||
//
|
||||
// Adding the ORDER BY clause slows down this query, particularly in a database where
|
||||
// the number of binary rules outweighs the number of certificate rules because:
|
||||
@@ -204,9 +234,9 @@ static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
//
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
FMResultSet *rs =
|
||||
[db executeQuery:
|
||||
@"SELECT * FROM rules WHERE (shasum=? and type=1) OR (shasum=? AND type=2) LIMIT 1",
|
||||
binarySHA256, certificateSHA256];
|
||||
[db executeQuery:@"SELECT * FROM rules WHERE (identifier=? and type=1) OR "
|
||||
@"(identifier=? AND type=2) OR (identifier=? AND type=3) LIMIT 1",
|
||||
binarySHA256, certificateSHA256, teamID];
|
||||
if ([rs next]) {
|
||||
rule = [self ruleFromResultSet:rs];
|
||||
}
|
||||
@@ -216,11 +246,11 @@ static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
// Allow binaries signed by the "Software Signing" cert used to sign launchd
|
||||
// if no existing rule has matched.
|
||||
if (!rule && [certificateSHA256 isEqual:self.launchdCSInfo.leafCertificate.SHA256]) {
|
||||
rule = [[SNTRule alloc] initWithShasum:certificateSHA256
|
||||
state:SNTRuleStateAllow
|
||||
type:SNTRuleTypeCertificate
|
||||
customMsg:nil
|
||||
timestamp:0];
|
||||
rule = [[SNTRule alloc] initWithIdentifier:certificateSHA256
|
||||
state:SNTRuleStateAllow
|
||||
type:SNTRuleTypeCertificate
|
||||
customMsg:nil
|
||||
timestamp:0];
|
||||
}
|
||||
|
||||
return rule;
|
||||
@@ -244,7 +274,7 @@ static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
}
|
||||
|
||||
for (SNTRule *rule in rules) {
|
||||
if (![rule isKindOfClass:[SNTRule class]] || rule.shasum.length == 0 ||
|
||||
if (![rule isKindOfClass:[SNTRule class]] || rule.identifier.length == 0 ||
|
||||
rule.state == SNTRuleStateUnknown || rule.type == SNTRuleTypeUnknown) {
|
||||
[self fillError:error code:SNTRuleTableErrorInvalidRule message:rule.description];
|
||||
*rollback = failed = YES;
|
||||
@@ -252,7 +282,7 @@ static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
}
|
||||
|
||||
if (rule.state == SNTRuleStateRemove) {
|
||||
if (![db executeUpdate:@"DELETE FROM rules WHERE shasum=? AND type=?", rule.shasum,
|
||||
if (![db executeUpdate:@"DELETE FROM rules WHERE identifier=? AND type=?", rule.identifier,
|
||||
@(rule.type)]) {
|
||||
[self fillError:error code:SNTRuleTableErrorRemoveFailed message:[db lastErrorMessage]];
|
||||
*rollback = failed = YES;
|
||||
@@ -260,9 +290,9 @@ static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
}
|
||||
} else {
|
||||
if (![db executeUpdate:@"INSERT OR REPLACE INTO rules "
|
||||
@"(shasum, state, type, custommsg, timestamp) "
|
||||
@"(identifier, state, type, custommsg, timestamp) "
|
||||
@"VALUES (?, ?, ?, ?, ?);",
|
||||
rule.shasum, @(rule.state), @(rule.type), rule.customMsg,
|
||||
rule.identifier, @(rule.state), @(rule.type), rule.customMsg,
|
||||
@(rule.timestamp)]) {
|
||||
[self fillError:error
|
||||
code:SNTRuleTableErrorInsertOrReplaceFailed
|
||||
@@ -284,7 +314,7 @@ static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
}
|
||||
|
||||
// If still here, then all rules in the array are allowlist rules. So now we look for allowlist
|
||||
// rules where there is a previously existing allowlist compiler rule for the same shasum.
|
||||
// rules where there is a previously existing allowlist compiler rule for the same identifier.
|
||||
// If so we find such a rule, then cache should be flushed.
|
||||
__block BOOL flushDecisionCache = NO;
|
||||
[self inTransaction:^(FMDatabase *db, BOOL *rollback) {
|
||||
@@ -293,8 +323,8 @@ static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
if (rule.type == SNTRuleTypeCertificate) continue;
|
||||
|
||||
if ([db longForQuery:
|
||||
@"SELECT COUNT(*) FROM rules WHERE shasum=? AND type=? AND state=? LIMIT 1",
|
||||
rule.shasum, @(SNTRuleTypeBinary), @(SNTRuleStateAllowCompiler)] > 0) {
|
||||
@"SELECT COUNT(*) FROM rules WHERE identifier=? AND type=? AND state=? LIMIT 1",
|
||||
rule.identifier, @(SNTRuleTypeBinary), @(SNTRuleStateAllowCompiler)] > 0) {
|
||||
flushDecisionCache = YES;
|
||||
break;
|
||||
}
|
||||
@@ -309,9 +339,9 @@ static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
if (!rule) return;
|
||||
[rule resetTimestamp];
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
if (![db executeUpdate:@"UPDATE rules SET timestamp=? WHERE shasum=? AND type=?",
|
||||
@(rule.timestamp), rule.shasum, @(rule.type)]) {
|
||||
LOGE(@"Could not update timestamp for rule with sha256=%@", rule.shasum);
|
||||
if (![db executeUpdate:@"UPDATE rules SET timestamp=? WHERE identifier=? AND type=?",
|
||||
@(rule.timestamp), rule.identifier, @(rule.type)]) {
|
||||
LOGE(@"Could not update timestamp for rule with sha256=%@", rule.identifier);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -34,9 +34,18 @@
|
||||
self.sut = [[SNTRuleTable alloc] initWithDatabaseQueue:self.dbq];
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleTeamIDRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"teamID";
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeTeamID;
|
||||
r.customMsg = @"A teamID rule";
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleBinaryRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.shasum = @"a";
|
||||
r.identifier = @"a";
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeBinary;
|
||||
r.customMsg = @"A rule";
|
||||
@@ -45,7 +54,7 @@
|
||||
|
||||
- (SNTRule *)_exampleCertRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.shasum = @"b";
|
||||
r.identifier = @"b";
|
||||
r.state = SNTRuleStateAllow;
|
||||
r.type = SNTRuleTypeCertificate;
|
||||
return r;
|
||||
@@ -103,7 +112,7 @@
|
||||
|
||||
- (void)testAddInvalidRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.shasum = @"a";
|
||||
r.identifier = @"a";
|
||||
r.type = SNTRuleTypeCertificate;
|
||||
|
||||
NSError *error;
|
||||
@@ -116,12 +125,12 @@
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:nil];
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.shasum, @"a");
|
||||
XCTAssertEqualObjects(r.identifier, @"a");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"b" certificateSHA256:nil];
|
||||
r = [self.sut ruleForBinarySHA256:@"b" certificateSHA256:nil teamID:nil];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
@@ -130,26 +139,52 @@
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"b"];
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"b" teamID:nil];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.shasum, @"b");
|
||||
XCTAssertEqualObjects(r.identifier, @"b");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"a"];
|
||||
r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchTeamIDRule {
|
||||
[self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleTeamIDRule] ]
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil certificateSHA256:nil teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID);
|
||||
XCTAssertEqual([self.sut teamIDRuleCount], 1);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil certificateSHA256:nil teamID:@"nonexistentTeamID"];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchRuleOrdering {
|
||||
[self.sut addRules:@[ [self _exampleCertRule], [self _exampleBinaryRule] ]
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
[self.sut
|
||||
addRules:@[ [self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule] ]
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
// This test verifies that the implicit rule ordering we've been abusing is still working.
|
||||
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:@"b"];
|
||||
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:@"b" teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.shasum, @"a");
|
||||
XCTAssertEqualObjects(r.identifier, @"a");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:@"unknowncert" teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"a");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown" certificateSHA256:@"b" teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"b");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
|
||||
}
|
||||
|
||||
- (void)testBadDatabase {
|
||||
|
||||
84
Source/santad/EventProviders/DiskArbitrationTestUtil.h
Normal file
84
Source/santad/EventProviders/DiskArbitrationTestUtil.h
Normal file
@@ -0,0 +1,84 @@
|
||||
/// Copyright 2021 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 <CoreFoundation/CFDictionary.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <DiskArbitration/DiskArbitration.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Mock object to point the opaque DADiskRefs to instead.
|
||||
// Note that this will have undefined behavior for DA functions that aren't
|
||||
// shimmed out by this utility, as the original DADiskRef refers to a completely
|
||||
// different struct managed by the CFRuntime.
|
||||
// https://opensource.apple.com/source/DiskArbitration/DiskArbitration-297.70.1/DiskArbitration/DADisk.c.auto.html
|
||||
@interface MockDADisk : NSObject
|
||||
@property(nonatomic) NSDictionary *diskDescription;
|
||||
@property(nonatomic, readwrite) NSString *name;
|
||||
@end
|
||||
|
||||
typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
|
||||
// Singleton mock fixture around all of the DiskArbitration framework functions
|
||||
@interface MockDiskArbitration : NSObject
|
||||
@property(nonatomic, readwrite, nonnull)
|
||||
NSMutableDictionary<NSString *, MockDADisk *> *insertedDevices;
|
||||
@property(nonatomic, readwrite, nonnull)
|
||||
NSMutableArray<MockDADiskAppearedCallback> *diskAppearedCallbacks;
|
||||
@property(nonatomic) BOOL wasRemounted;
|
||||
@property(nonatomic, nullable) dispatch_queue_t sessionQueue;
|
||||
|
||||
- (instancetype _Nonnull)init;
|
||||
- (void)reset;
|
||||
|
||||
// Also triggers DADiskRegisterDiskAppearedCallback
|
||||
- (void)insert:(MockDADisk *)ref bsdName:(NSString *)bsdName;
|
||||
|
||||
// Retrieve an initialized singleton MockDiskArbitration object
|
||||
+ (instancetype _Nonnull)mockDiskArbitration;
|
||||
@end
|
||||
|
||||
//
|
||||
// All DiskArbitration functions used in SNTDeviceManager and shimmed out accordingly.
|
||||
//
|
||||
CF_EXTERN_C_BEGIN
|
||||
|
||||
void DADiskMountWithArguments(DADiskRef _Nonnull disk, CFURLRef __nullable path,
|
||||
DADiskMountOptions options, DADiskMountCallback __nullable callback,
|
||||
void *__nullable context,
|
||||
CFStringRef __nullable arguments[_Nullable]);
|
||||
|
||||
DADiskRef __nullable DADiskCreateFromBSDName(CFAllocatorRef __nullable allocator,
|
||||
DASessionRef session, const char *name);
|
||||
|
||||
CFDictionaryRef __nullable DADiskCopyDescription(DADiskRef disk);
|
||||
|
||||
void DARegisterDiskAppearedCallback(DASessionRef session, CFDictionaryRef __nullable match,
|
||||
DADiskAppearedCallback callback, void *__nullable context);
|
||||
|
||||
void DARegisterDiskDisappearedCallback(DASessionRef session, CFDictionaryRef __nullable match,
|
||||
DADiskDisappearedCallback callback,
|
||||
void *__nullable context);
|
||||
|
||||
void DARegisterDiskDescriptionChangedCallback(DASessionRef session,
|
||||
CFDictionaryRef __nullable match,
|
||||
CFArrayRef __nullable watch,
|
||||
DADiskDescriptionChangedCallback callback,
|
||||
void *__nullable context);
|
||||
|
||||
void DASessionSetDispatchQueue(DASessionRef session, dispatch_queue_t __nullable queue);
|
||||
DASessionRef __nullable DASessionCreate(CFAllocatorRef __nullable allocator);
|
||||
|
||||
CF_EXTERN_C_END
|
||||
NS_ASSUME_NONNULL_END
|
||||
120
Source/santad/EventProviders/DiskArbitrationTestUtil.mm
Normal file
120
Source/santad/EventProviders/DiskArbitrationTestUtil.mm
Normal file
@@ -0,0 +1,120 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation MockDADisk
|
||||
@end
|
||||
|
||||
@implementation MockDiskArbitration
|
||||
|
||||
- (instancetype _Nonnull)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_insertedDevices = [NSMutableDictionary dictionary];
|
||||
_diskAppearedCallbacks = [NSMutableArray array];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
[self.insertedDevices removeAllObjects];
|
||||
[self.diskAppearedCallbacks removeAllObjects];
|
||||
self.sessionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
self.wasRemounted = NO;
|
||||
}
|
||||
|
||||
- (void)insert:(MockDADisk *)ref bsdName:(NSString *)bsdName {
|
||||
self.insertedDevices[bsdName] = ref;
|
||||
|
||||
for (MockDADiskAppearedCallback callback in self.diskAppearedCallbacks) {
|
||||
dispatch_sync(self.sessionQueue, ^{
|
||||
callback((__bridge DADiskRef)ref);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve an initialized singleton MockDiskArbitration object
|
||||
+ (instancetype _Nonnull)mockDiskArbitration {
|
||||
static MockDiskArbitration *sharedES;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedES = [[MockDiskArbitration alloc] init];
|
||||
});
|
||||
return sharedES;
|
||||
};
|
||||
|
||||
@end
|
||||
|
||||
void DADiskMountWithArguments(DADiskRef _Nonnull disk, CFURLRef __nullable path,
|
||||
DADiskMountOptions options, DADiskMountCallback __nullable callback,
|
||||
void *__nullable context,
|
||||
CFStringRef __nullable arguments[_Nullable]) {
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
mockDA.wasRemounted = YES;
|
||||
}
|
||||
|
||||
DADiskRef __nullable DADiskCreateFromBSDName(CFAllocatorRef __nullable allocator,
|
||||
DASessionRef session, const char *name) {
|
||||
NSString *nsName = [NSString stringWithUTF8String:name];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
MockDADisk *got = mockDA.insertedDevices[nsName];
|
||||
DADiskRef ref = (__bridge DADiskRef)got;
|
||||
CFRetain(ref);
|
||||
return ref;
|
||||
}
|
||||
|
||||
CFDictionaryRef __nullable DADiskCopyDescription(DADiskRef disk) {
|
||||
CFDictionaryRef description = NULL;
|
||||
if (disk) {
|
||||
MockDADisk *mockDisk = (__bridge MockDADisk *)disk;
|
||||
description = (__bridge_retained CFDictionaryRef)mockDisk.diskDescription;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
void DARegisterDiskAppearedCallback(DASessionRef session, CFDictionaryRef __nullable match,
|
||||
DADiskAppearedCallback callback, void *__nullable context) {
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA.diskAppearedCallbacks addObject:^(DADiskRef ref) {
|
||||
callback(ref, context);
|
||||
}];
|
||||
}
|
||||
|
||||
void DARegisterDiskDisappearedCallback(DASessionRef session, CFDictionaryRef __nullable match,
|
||||
DADiskDisappearedCallback callback,
|
||||
void *__nullable context){};
|
||||
|
||||
void DARegisterDiskDescriptionChangedCallback(DASessionRef session,
|
||||
CFDictionaryRef __nullable match,
|
||||
CFArrayRef __nullable watch,
|
||||
DADiskDescriptionChangedCallback callback,
|
||||
void *__nullable context){};
|
||||
|
||||
void DASessionSetDispatchQueue(DASessionRef session, dispatch_queue_t __nullable queue) {
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
mockDA.sessionQueue = queue;
|
||||
};
|
||||
|
||||
DASessionRef __nullable DASessionCreate(CFAllocatorRef __nullable allocator) {
|
||||
return (__bridge DASessionRef)[MockDiskArbitration mockDiskArbitration];
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -46,7 +46,7 @@ typedef void (^ESCallback)(ESResponse *_Nonnull);
|
||||
@interface MockEndpointSecurity : NSObject
|
||||
@property NSMutableArray *_Nonnull subscriptions;
|
||||
- (void)reset;
|
||||
- (void)registerResponseCallback:(ESCallback _Nonnull)callback;
|
||||
- (void)registerResponseCallback:(es_event_type_t)t withCallback:(ESCallback _Nonnull)callback;
|
||||
- (void)triggerHandler:(es_message_t *_Nonnull)msg;
|
||||
|
||||
/// Retrieve an initialized singleton MockEndpointSecurity object
|
||||
|
||||
@@ -89,19 +89,22 @@ CF_EXTERN_C_END
|
||||
@implementation ESResponse
|
||||
@end
|
||||
|
||||
@interface MockEndpointSecurity ()
|
||||
@property NSMutableArray<ESCallback> *responseCallbacks;
|
||||
@property NSObject *client;
|
||||
@interface MockESClient : NSObject
|
||||
@property NSMutableArray *_Nonnull subscriptions;
|
||||
@property es_handler_block_t handler;
|
||||
@end
|
||||
|
||||
@implementation MockEndpointSecurity
|
||||
@implementation MockESClient
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_responseCallbacks = [NSMutableArray array];
|
||||
_subscriptions = [NSMutableArray arrayWithCapacity:ES_EVENT_TYPE_LAST];
|
||||
[self resetSubscriptions];
|
||||
@synchronized(self) {
|
||||
_subscriptions = [NSMutableArray arrayWithCapacity:ES_EVENT_TYPE_LAST];
|
||||
for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) {
|
||||
[self.subscriptions addObject:@NO];
|
||||
}
|
||||
}
|
||||
}
|
||||
return self;
|
||||
};
|
||||
@@ -112,31 +115,78 @@ CF_EXTERN_C_END
|
||||
}
|
||||
}
|
||||
|
||||
- (void)triggerHandler:(es_message_t *_Nonnull)msg {
|
||||
self.handler((__bridge es_client_t *_Nullable)self, msg);
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
@synchronized(self) {
|
||||
[self.subscriptions removeAllObjects];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface MockEndpointSecurity ()
|
||||
@property NSMutableArray<MockESClient *> *clients;
|
||||
|
||||
// Array of collections of ESCallback blocks
|
||||
// This should be of size ES_EVENT_TYPE_LAST, allowing for indexing by ES_EVENT_TYPE_xxx members.
|
||||
@property NSMutableArray<NSMutableArray<ESCallback> *> *responseCallbacks;
|
||||
@end
|
||||
|
||||
@implementation MockEndpointSecurity
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
@synchronized(self) {
|
||||
_clients = [NSMutableArray array];
|
||||
_responseCallbacks = [NSMutableArray arrayWithCapacity:ES_EVENT_TYPE_LAST];
|
||||
for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) {
|
||||
[self.responseCallbacks addObject:[NSMutableArray array]];
|
||||
}
|
||||
[self reset];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
};
|
||||
|
||||
- (void)resetResponseCallbacks {
|
||||
for (NSMutableArray *callback in self.responseCallbacks) {
|
||||
if (callback != nil) {
|
||||
[callback removeAllObjects];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
@synchronized(self) {
|
||||
[self.responseCallbacks removeAllObjects];
|
||||
self.handler = nil;
|
||||
self.client = nil;
|
||||
[self.clients removeAllObjects];
|
||||
[self resetResponseCallbacks];
|
||||
}
|
||||
};
|
||||
|
||||
- (void)newClient:(es_client_t *_Nullable *_Nonnull)client
|
||||
handler:(es_handler_block_t __strong)handler {
|
||||
// es_client_t is generally used as a pointer to an opaque struct (secretly a mach port).
|
||||
// We just want to set it to something nonnull for passing initialization checks. It shouldn't
|
||||
// ever be directly dereferenced.
|
||||
self.client = [[NSObject alloc] init];
|
||||
*client = (__bridge es_client_t *)self.client;
|
||||
self.handler = handler;
|
||||
// There is also a few nonnull initialization checks on it.
|
||||
MockESClient *mockClient = [[MockESClient alloc] init];
|
||||
*client = (__bridge es_client_t *)mockClient;
|
||||
mockClient.handler = handler;
|
||||
[self.clients addObject:mockClient];
|
||||
}
|
||||
|
||||
- (void)triggerHandler:(es_message_t *_Nonnull)msg {
|
||||
self.handler((__bridge es_client_t *_Nullable)self.client, msg);
|
||||
for (MockESClient *client in self.clients) {
|
||||
if (client.subscriptions[msg->event_type]) {
|
||||
[client triggerHandler:msg];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)registerResponseCallback:(ESCallback _Nonnull)callback {
|
||||
- (void)registerResponseCallback:(es_event_type_t)t withCallback:(ESCallback _Nonnull)callback {
|
||||
@synchronized(self) {
|
||||
[self.responseCallbacks addObject:callback];
|
||||
[self.responseCallbacks[t] addObject:callback];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +197,7 @@ CF_EXTERN_C_END
|
||||
ESResponse *response = [[ESResponse alloc] init];
|
||||
response.result = result;
|
||||
response.shouldCache = cache;
|
||||
for (void (^callback)(ESResponse *) in self.responseCallbacks) {
|
||||
for (void (^callback)(ESResponse *) in self.responseCallbacks[msg->event_type]) {
|
||||
callback(response);
|
||||
}
|
||||
}
|
||||
@@ -156,10 +206,22 @@ CF_EXTERN_C_END
|
||||
|
||||
- (void)setSubscriptions:(const es_event_type_t *_Nonnull)events
|
||||
event_count:(uint32_t)event_count
|
||||
value:(NSNumber *)value {
|
||||
value:(NSNumber *)value
|
||||
client:(es_client_t *)client {
|
||||
@synchronized(self) {
|
||||
MockESClient *toUpdate = nil;
|
||||
for (MockESClient *c in self.clients) {
|
||||
if (client == (__bridge es_client_t *)c) {
|
||||
toUpdate = c;
|
||||
}
|
||||
}
|
||||
if (toUpdate == nil) {
|
||||
NSLog(@"setting subscription for unknown client");
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < event_count; i++) {
|
||||
self.subscriptions[events[i]] = value;
|
||||
toUpdate.subscriptions[events[i]] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,7 +274,8 @@ es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_N
|
||||
uint32_t event_count) {
|
||||
[[MockEndpointSecurity mockEndpointSecurity] setSubscriptions:events
|
||||
event_count:event_count
|
||||
value:@YES];
|
||||
value:@YES
|
||||
client:client];
|
||||
return ES_RETURN_SUCCESS;
|
||||
}
|
||||
API_AVAILABLE(macos(10.15))
|
||||
@@ -221,7 +284,8 @@ es_return_t es_unsubscribe(es_client_t *_Nonnull client, const es_event_type_t *
|
||||
uint32_t event_count) {
|
||||
[[MockEndpointSecurity mockEndpointSecurity] setSubscriptions:events
|
||||
event_count:event_count
|
||||
value:@NO];
|
||||
value:@NO
|
||||
client:client];
|
||||
|
||||
return ES_RETURN_SUCCESS;
|
||||
};
|
||||
|
||||
33
Source/santad/EventProviders/SNTDeviceManager.h
Normal file
33
Source/santad/EventProviders/SNTDeviceManager.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
/*
|
||||
* Manages DiskArbitration and EndpointSecurity to monitor/block/remount USB
|
||||
* storage devices.
|
||||
*/
|
||||
@interface SNTDeviceManager : NSObject
|
||||
|
||||
@property(nonatomic, readwrite) BOOL subscribed;
|
||||
@property(nonatomic, readwrite) BOOL blockUSBMount;
|
||||
@property(nonatomic, readwrite, nullable) NSArray<NSString *> *remountArgs;
|
||||
|
||||
- (instancetype _Nonnull)init;
|
||||
- (void)listen;
|
||||
- (BOOL)subscribed;
|
||||
|
||||
@end
|
||||
300
Source/santad/EventProviders/SNTDeviceManager.mm
Normal file
300
Source/santad/EventProviders/SNTDeviceManager.mm
Normal file
@@ -0,0 +1,300 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import "Source/santad/EventProviders/SNTDeviceManager.h"
|
||||
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <errno.h>
|
||||
#include <libproc.h>
|
||||
#include <sys/mount.h>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/santad/Logs/SNTEventLog.h"
|
||||
|
||||
void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
|
||||
if (dissenter) {
|
||||
DAReturn status = DADissenterGetStatus(dissenter);
|
||||
|
||||
NSString *statusString = (NSString *)DADissenterGetStatusString(dissenter);
|
||||
IOReturn systemCode = err_get_system(status);
|
||||
IOReturn subSystemCode = err_get_sub(status);
|
||||
IOReturn errorCode = err_get_code(status);
|
||||
|
||||
LOGE(
|
||||
@"SNTDeviceManager: dissenter status codes: system: %d, subsystem: %d, err: %d; status: %s",
|
||||
systemCode, subSystemCode, errorCode, [statusString UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
void diskAppearedCallback(DADiskRef disk, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
SNTEventLog *logger = [SNTEventLog logger];
|
||||
if (logger) [logger logDiskAppeared:props];
|
||||
}
|
||||
|
||||
void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
|
||||
if (props[@"DAVolumePath"]) {
|
||||
SNTEventLog *logger = [SNTEventLog logger];
|
||||
if (logger) [logger logDiskAppeared:props];
|
||||
}
|
||||
}
|
||||
|
||||
void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
|
||||
SNTEventLog *logger = [SNTEventLog logger];
|
||||
if (logger) [logger logDiskDisappeared:props];
|
||||
}
|
||||
|
||||
NSArray<NSString *> *maskToMountArgs(long remountOpts) {
|
||||
NSMutableArray<NSString *> *args = [NSMutableArray array];
|
||||
if (remountOpts & MNT_RDONLY) [args addObject:@"rdonly"];
|
||||
if (remountOpts & MNT_NOEXEC) [args addObject:@"noexec"];
|
||||
if (remountOpts & MNT_NOSUID) [args addObject:@"nosuid"];
|
||||
if (remountOpts & MNT_DONTBROWSE) [args addObject:@"nobrowse"];
|
||||
if (remountOpts & MNT_UNKNOWNPERMISSIONS) [args addObject:@"noowners"];
|
||||
if (remountOpts & MNT_NODEV) [args addObject:@"nodev"];
|
||||
if (remountOpts & MNT_JOURNALED) [args addObject:@"-j"];
|
||||
if (remountOpts & MNT_ASYNC) [args addObject:@"async"];
|
||||
return args;
|
||||
}
|
||||
|
||||
long mountArgsToMask(NSArray<NSString *> *args) {
|
||||
long flags = 0;
|
||||
for (NSString *i in args) {
|
||||
NSString *arg = [i lowercaseString];
|
||||
if ([arg isEqualToString:@"rdonly"])
|
||||
flags |= MNT_RDONLY;
|
||||
else if ([arg isEqualToString:@"noexec"])
|
||||
flags |= MNT_NOEXEC;
|
||||
else if ([arg isEqualToString:@"nosuid"])
|
||||
flags |= MNT_NOSUID;
|
||||
else if ([arg isEqualToString:@"nobrowse"])
|
||||
flags |= MNT_DONTBROWSE;
|
||||
else if ([arg isEqualToString:@"noowners"])
|
||||
flags |= MNT_UNKNOWNPERMISSIONS;
|
||||
else if ([arg isEqualToString:@"nodev"])
|
||||
flags |= MNT_NODEV;
|
||||
else if ([arg isEqualToString:@"-j"])
|
||||
flags |= MNT_JOURNALED;
|
||||
else if ([arg isEqualToString:@"async"])
|
||||
flags |= MNT_ASYNC;
|
||||
else
|
||||
LOGE(@"SNTDeviceManager: unexpected mount arg: %@", arg);
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
@interface SNTDeviceManager ()
|
||||
|
||||
@property DASessionRef diskArbSession;
|
||||
@property(nonatomic, readonly) es_client_t *client;
|
||||
@property(nonatomic, readonly) dispatch_queue_t esAuthQueue;
|
||||
@property(nonatomic, readonly) dispatch_queue_t diskQueue;
|
||||
@end
|
||||
|
||||
@implementation SNTDeviceManager
|
||||
|
||||
- (instancetype _Nonnull)init API_AVAILABLE(macos(10.15)) {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_blockUSBMount = false;
|
||||
|
||||
_diskQueue = dispatch_queue_create("com.google.santad.disk_queue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_esAuthQueue =
|
||||
dispatch_queue_create("com.google.santa.daemon.es_device_auth", DISPATCH_QUEUE_CONCURRENT);
|
||||
|
||||
_diskArbSession = DASessionCreate(NULL);
|
||||
DASessionSetDispatchQueue(_diskArbSession, _diskQueue);
|
||||
|
||||
if (@available(macos 10.15, *)) [self initES];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initES API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.client) {
|
||||
es_client_t *client = NULL;
|
||||
es_new_client_result_t ret = es_new_client(&client, ^(es_client_t *c, const es_message_t *m) {
|
||||
// Set timeout to 5 seconds before the ES deadline.
|
||||
[self handleESMessageWithTimeout:m
|
||||
withClient:c
|
||||
timeout:dispatch_time(m->deadline, NSEC_PER_SEC * -5)];
|
||||
});
|
||||
|
||||
switch (ret) {
|
||||
case ES_NEW_CLIENT_RESULT_SUCCESS:
|
||||
LOGI(@"Connected to EndpointSecurity");
|
||||
_client = client;
|
||||
return;
|
||||
case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED:
|
||||
LOGE(@"Unable to create EndpointSecurity client, not full-disk access permitted");
|
||||
LOGE(@"Sleeping for 30s before restarting.");
|
||||
sleep(30);
|
||||
exit(ret);
|
||||
default:
|
||||
LOGE(@"Unable to create es client: %d. Sleeping for a minute.", ret);
|
||||
sleep(60);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)listenES API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.client)
|
||||
usleep(100000); // 100ms
|
||||
|
||||
es_event_type_t events[] = {
|
||||
ES_EVENT_TYPE_AUTH_MOUNT,
|
||||
};
|
||||
|
||||
es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t));
|
||||
if (sret != ES_RETURN_SUCCESS)
|
||||
LOGE(@"SNTDeviceManager: unable to subscribe to auth mount events: %d", sret);
|
||||
}
|
||||
|
||||
- (void)listenDA {
|
||||
DARegisterDiskAppearedCallback(_diskArbSession, NULL, diskAppearedCallback,
|
||||
(__bridge void *)self);
|
||||
DARegisterDiskDescriptionChangedCallback(_diskArbSession, NULL, NULL,
|
||||
diskDescriptionChangedCallback, (__bridge void *)self);
|
||||
DARegisterDiskDisappearedCallback(_diskArbSession, NULL, diskDisappearedCallback,
|
||||
(__bridge void *)self);
|
||||
}
|
||||
|
||||
- (void)listen {
|
||||
[self listenDA];
|
||||
if (@available(macos 10.15, *)) [self listenES];
|
||||
self.subscribed = YES;
|
||||
}
|
||||
|
||||
- (void)handleAuthMount:(const es_message_t *)m
|
||||
withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) {
|
||||
if (!self.blockUSBMount) {
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
return;
|
||||
}
|
||||
|
||||
long mountMode = m->event.mount.statfs->f_flags;
|
||||
pid_t pid = audit_token_to_pid(m->process->audit_token);
|
||||
LOGI(@"SNTDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
|
||||
m->process->executable->path.data, pid, mountMode);
|
||||
|
||||
DADiskRef disk =
|
||||
DADiskCreateFromBSDName(NULL, self.diskArbSession, m->event.mount.statfs->f_mntfromname);
|
||||
CFAutorelease(disk);
|
||||
|
||||
// TODO(tnek): Log all of the other attributes available in diskInfo into a structured log format.
|
||||
NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
BOOL isRemovable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaRemovableKey] boolValue];
|
||||
BOOL isUSB =
|
||||
[diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey] isEqualTo:@"USB"];
|
||||
|
||||
if (!isRemovable || !isUSB) {
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL shouldRemount = self.remountArgs != nil && [self.remountArgs count] > 0;
|
||||
|
||||
if (shouldRemount) {
|
||||
long remountOpts = mountArgsToMask(self.remountArgs);
|
||||
if (mountMode & remountOpts) {
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
return;
|
||||
}
|
||||
|
||||
long newMode = mountMode | remountOpts;
|
||||
LOGI(@"SNTDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)",
|
||||
m->event.mount.statfs->f_mntfromname, m->event.mount.statfs->f_mntonname, mountMode,
|
||||
newMode);
|
||||
[self remount:disk mountMode:newMode];
|
||||
}
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
|
||||
}
|
||||
|
||||
- (void)remount:(DADiskRef)disk mountMode:(long)remountMask {
|
||||
NSArray<NSString *> *args = maskToMountArgs(remountMask);
|
||||
CFStringRef *argv = (CFStringRef *)calloc(args.count + 1, sizeof(CFStringRef));
|
||||
CFArrayGetValues((__bridge CFArrayRef)args, CFRangeMake(0, (CFIndex)args.count),
|
||||
(const void **)argv);
|
||||
|
||||
DADiskMountWithArguments(disk, NULL, kDADiskMountOptionDefault, diskMountedCallback,
|
||||
(__bridge void *)self, (CFStringRef *)argv);
|
||||
|
||||
free(argv);
|
||||
}
|
||||
|
||||
// handleESMessage handles an ES message synchronously. This will block all incoming ES events
|
||||
// until either we serve a response or we hit the auth deadline. Prefer [SNTDeviceManager
|
||||
// handleESMessageWithTimeout]
|
||||
// TODO(tnek): generalize this timeout handling logic so that EndpointSecurityManager can use it
|
||||
// too.
|
||||
- (void)handleESMessageWithTimeout:(const es_message_t *)m
|
||||
withClient:(es_client_t *)c
|
||||
timeout:(dispatch_time_t)timeout API_AVAILABLE(macos(10.15)) {
|
||||
// ES will kill our whole client if we don't meet the es_message auth deadline, so we try to
|
||||
// gracefully handle it with a deny-by-default in the worst-case before it can do that.
|
||||
// This isn't an issue for notify events, so we're in no rush for those.
|
||||
std::shared_ptr<std::atomic<bool>> responded;
|
||||
if (m->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
responded = std::make_shared<std::atomic<bool>>(false);
|
||||
dispatch_after(timeout, self.esAuthQueue, ^(void) {
|
||||
if (responded->load()) return;
|
||||
LOGE(@"SNTDeviceManager: deadline reached: deny pid=%d ret=%d",
|
||||
audit_token_to_pid(m->process->audit_token),
|
||||
es_respond_auth_result(c, m, ES_AUTH_RESULT_DENY, false));
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(tnek): migrate to es_retain_message.
|
||||
es_message_t *mc = es_copy_message(m);
|
||||
dispatch_async(self.esAuthQueue, ^{
|
||||
[self handleESMessage:m withClient:c];
|
||||
|
||||
if (m->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
responded->store(true);
|
||||
}
|
||||
|
||||
es_free_message(mc);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)handleESMessage:(const es_message_t *)m
|
||||
withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) {
|
||||
switch (m->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_MOUNT: {
|
||||
[self handleAuthMount:m withClient:c];
|
||||
// Intentional fallthrough
|
||||
[[fallthrough]];
|
||||
}
|
||||
// TODO(tnek): log any extra data here about mounts.
|
||||
case ES_EVENT_TYPE_NOTIFY_MOUNT: {
|
||||
break;
|
||||
}
|
||||
default: LOGE(@"SNTDeviceManager: unexpected event type: %d", m->event_type);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
157
Source/santad/EventProviders/SNTDeviceManagerTest.mm
Normal file
157
Source/santad/EventProviders/SNTDeviceManagerTest.mm
Normal file
@@ -0,0 +1,157 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <bsm/libbsm.h>
|
||||
|
||||
#include <sys/mount.h>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/santad/EventProviders/SNTDeviceManager.h"
|
||||
|
||||
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
|
||||
#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h"
|
||||
|
||||
@interface SNTDeviceManagerTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@end
|
||||
|
||||
@implementation SNTDeviceManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator eventLogType]).andReturn(-1);
|
||||
|
||||
fclose(stdout);
|
||||
}
|
||||
|
||||
- (ESResponse *)triggerTestMount:(SNTDeviceManager *)deviceManager
|
||||
mockES:(MockEndpointSecurity *)mockES
|
||||
mockDA:(MockDiskArbitration *)mockDA {
|
||||
if (!deviceManager.subscribed) {
|
||||
// [deviceManager listen] is synchronous, but we want to asynchronously dispatch it
|
||||
// with an enforced timeout to ensure that we never run into issues where the client
|
||||
// never instantiates.
|
||||
XCTestExpectation *initExpectation =
|
||||
[self expectationWithDescription:@"Wait for SNTDeviceManager to instantiate"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
[deviceManager listen];
|
||||
});
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
while (!deviceManager.subscribed)
|
||||
;
|
||||
[initExpectation fulfill];
|
||||
});
|
||||
[self waitForExpectations:@[ initExpectation ] timeout:60.0];
|
||||
}
|
||||
|
||||
struct statfs *fs = static_cast<struct statfs *>(calloc(1, sizeof(struct statfs)));
|
||||
NSString *test_mntfromname = @"/dev/disk2s1";
|
||||
NSString *test_mntonname = @"/Volumes/KATE'S 4G";
|
||||
const char *c_mntfromname = [test_mntfromname UTF8String];
|
||||
const char *c_mntonname = [test_mntonname UTF8String];
|
||||
|
||||
strncpy(fs->f_mntfromname, c_mntfromname, MAXPATHLEN);
|
||||
strncpy(fs->f_mntonname, c_mntonname, MAXPATHLEN);
|
||||
|
||||
MockDADisk *disk = [[MockDADisk alloc] init];
|
||||
disk.diskDescription = @{
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"USB",
|
||||
(__bridge NSString *)kDADiskDescriptionMediaRemovableKey : @YES,
|
||||
@"DAVolumeMountable" : @YES,
|
||||
@"DAVolumePath" : test_mntonname,
|
||||
@"DADeviceModel" : @"Some device model",
|
||||
@"DADevicePath" : test_mntonname,
|
||||
@"DADeviceVendor" : @"Some vendor",
|
||||
@"DAAppearanceTime" : @0,
|
||||
@"DAMediaBSDName" : test_mntfromname,
|
||||
};
|
||||
|
||||
[mockDA insert:disk bsdName:test_mntfromname];
|
||||
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = @"/System/Library/Filesystems/msdos.fs/Contents/Resources/mount_msdos";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_MOUNT;
|
||||
m.message->event = (es_events_t){.mount = {.statfs = fs}};
|
||||
}];
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
free(fs);
|
||||
|
||||
return got;
|
||||
}
|
||||
|
||||
- (void)testUSBBlockDisabled {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = NO;
|
||||
ESResponse *got = [self triggerTestMount:deviceManager mockES:mockES mockDA:mockDA];
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
|
||||
- (void)testRemount {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = YES;
|
||||
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
ESResponse *got = [self triggerTestMount:deviceManager mockES:mockES mockDA:mockDA];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
|
||||
XCTAssertEqual(mockDA.wasRemounted, YES);
|
||||
}
|
||||
|
||||
- (void)testBlockNoRemount {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = YES;
|
||||
|
||||
ESResponse *got = [self triggerTestMount:deviceManager mockES:mockES mockDA:mockDA];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
|
||||
XCTAssertEqual(mockDA.wasRemounted, NO);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -352,6 +352,7 @@
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
|
||||
return;
|
||||
}
|
||||
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: {
|
||||
sm.action = ACTION_NOTIFY_WRITE;
|
||||
targetFile = m->event.close.target;
|
||||
|
||||
@@ -49,15 +49,16 @@ const NSString *const kBenignPath = @"/some/other/path";
|
||||
[self expectationWithDescription:@"Wait for santa's Auth dispatch queue"];
|
||||
|
||||
__block NSMutableArray<ESResponse *> *events = [NSMutableArray array];
|
||||
[mockES registerResponseCallback:^(ESResponse *r) {
|
||||
@synchronized(self) {
|
||||
[events addObject:r];
|
||||
}
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
|
||||
withCallback:^(ESResponse *r) {
|
||||
@synchronized(self) {
|
||||
[events addObject:r];
|
||||
}
|
||||
|
||||
if (events.count >= wantNumResp) {
|
||||
[expectation fulfill];
|
||||
}
|
||||
}];
|
||||
if (events.count >= wantNumResp) {
|
||||
[expectation fulfill];
|
||||
}
|
||||
}];
|
||||
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(kEventsDBPath)};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
@@ -94,10 +95,11 @@ const NSString *const kBenignPath = @"/some/other/path";
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
@@ -125,10 +127,11 @@ const NSString *const kBenignPath = @"/some/other/path";
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(@"/some/other/path")};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
@@ -160,10 +163,11 @@ const NSString *const kBenignPath = @"/some/other/path";
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_RENAME
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t otherFile = {.path = MakeStringToken(@"/some/other/path")};
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
|
||||
@@ -206,10 +210,11 @@ const NSString *const kBenignPath = @"/some/other/path";
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_RENAME
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t otherFile = {.path = MakeStringToken(@"/some/other/path")};
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
|
||||
|
||||
@@ -14,15 +14,20 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTKernelCommon.h"
|
||||
#include "Source/common/SNTKernelCommon.h"
|
||||
|
||||
@class SNTCachedDecision;
|
||||
@class SNTStoredEvent;
|
||||
|
||||
///
|
||||
/// Logs execution and file write events to syslog
|
||||
/// Abstract interface for logging execution and file write events to syslog
|
||||
///
|
||||
@interface SNTEventLog : NSObject
|
||||
|
||||
// Getter for a singleton SNTEventLog object.
|
||||
// Determines which type of SNTEventLog to use based on [SNTConfigurator eventLogType].
|
||||
+ (instancetype)logger;
|
||||
|
||||
// Methods implemented by a concrete subclass.
|
||||
- (void)logDiskAppeared:(NSDictionary *)diskProperties;
|
||||
- (void)logDiskDisappeared:(NSDictionary *)diskProperties;
|
||||
@@ -34,7 +39,7 @@
|
||||
- (void)logExit:(santa_message_t)message;
|
||||
- (void)writeLog:(NSString *)log;
|
||||
|
||||
// Methods for storing, retrieving, and removing cached decisions.
|
||||
// Methods for storing, retrieving, and removing cached decisions.
|
||||
- (void)cacheDecision:(SNTCachedDecision *)cd;
|
||||
- (SNTCachedDecision *)cachedDecisionForMessage:(santa_message_t)message;
|
||||
- (void)forgetCachedDecisionForVnodeId:(santa_vnode_id_t)vnodeId;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/Logs/SNTEventLog.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
@@ -25,6 +24,9 @@
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
|
||||
#import "Source/santad/Logs/SNTFileEventLog.h"
|
||||
#import "Source/santad/Logs/SNTSyslogEventLog.h"
|
||||
|
||||
@interface SNTEventLog ()
|
||||
@property NSMutableDictionary<NSNumber *, SNTCachedDecision *> *detailStore;
|
||||
@property dispatch_queue_t detailStoreQueue;
|
||||
@@ -50,6 +52,7 @@
|
||||
|
||||
_dateFormatter = [[NSDateFormatter alloc] init];
|
||||
_dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
|
||||
_dateFormatter.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601];
|
||||
_dateFormatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
|
||||
|
||||
// Grab the system UUID on init
|
||||
@@ -120,10 +123,10 @@
|
||||
if (cd.decision != SNTEventStateAllowTransitive) return;
|
||||
NSDate *lastUpdate = [self.timestampResetMap objectForKey:cd.sha256];
|
||||
if (!lastUpdate || -[lastUpdate timeIntervalSinceNow] > 3600) {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithShasum:cd.sha256
|
||||
state:SNTRuleStateAllowTransitive
|
||||
type:SNTRuleTypeBinary
|
||||
customMsg:nil];
|
||||
SNTRule *rule = [[SNTRule alloc] initWithIdentifier:cd.sha256
|
||||
state:SNTRuleStateAllowTransitive
|
||||
type:SNTRuleTypeBinary
|
||||
customMsg:nil];
|
||||
[[SNTDatabaseController ruleTable] resetTimestampForRule:rule];
|
||||
[self.timestampResetMap setObject:[NSDate date] forKey:cd.sha256];
|
||||
}
|
||||
@@ -418,4 +421,22 @@
|
||||
return [origURL path]; // this will be nil if there was an error
|
||||
}
|
||||
|
||||
+ (instancetype)logger {
|
||||
static SNTEventLog *logger;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
switch ([[SNTConfigurator configurator] eventLogType]) {
|
||||
case SNTEventLogTypeSyslog: {
|
||||
logger = [[SNTSyslogEventLog alloc] init];
|
||||
break;
|
||||
}
|
||||
case SNTEventLogTypeFilelog: {
|
||||
logger = [[SNTFileEventLog alloc] init];
|
||||
break;
|
||||
}
|
||||
default: logger = nil;
|
||||
}
|
||||
});
|
||||
return logger;
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -62,8 +62,15 @@
|
||||
if (newpath) {
|
||||
[outStr appendFormat:@"|newpath=%@", [self sanitizeString:newpath]];
|
||||
}
|
||||
|
||||
char ppath[PATH_MAX] = "(null)";
|
||||
proc_pidpath(message.pid, ppath, PATH_MAX);
|
||||
if (message.es_message) {
|
||||
es_message_t *m = message.es_message;
|
||||
es_string_token_t path = m->process->executable->path;
|
||||
strlcpy(ppath, path.data, sizeof(ppath));
|
||||
} else {
|
||||
proc_pidpath(message.pid, ppath, PATH_MAX);
|
||||
}
|
||||
|
||||
[outStr
|
||||
appendFormat:
|
||||
@@ -113,6 +120,11 @@
|
||||
r = @"SCOPE";
|
||||
logArgs = YES;
|
||||
break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
d = @"ALLOW";
|
||||
r = @"TEAMID";
|
||||
logArgs = YES;
|
||||
break;
|
||||
case SNTEventStateAllowUnknown:
|
||||
d = @"ALLOW";
|
||||
r = @"UNKNOWN";
|
||||
@@ -130,6 +142,10 @@
|
||||
d = @"DENY";
|
||||
r = @"SCOPE";
|
||||
break;
|
||||
case SNTEventStateBlockTeamID:
|
||||
d = @"DENY";
|
||||
r = @"TEAMID";
|
||||
break;
|
||||
case SNTEventStateBlockUnknown:
|
||||
d = @"DENY";
|
||||
r = @"UNKNOWN";
|
||||
|
||||
@@ -13,25 +13,27 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/SNTApplication.h"
|
||||
#import "Source/santad/SNTApplicationCoreMetrics.h"
|
||||
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDropRootPrivs.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/common/SNTXPCMetricServiceInterface.h"
|
||||
#import "Source/common/SNTXPCNotifierInterface.h"
|
||||
#import "Source/common/SNTXPCUnprivilegedControlInterface.h"
|
||||
#import "Source/santad/DataLayer/SNTEventTable.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#import "Source/santad/EventProviders/SNTCachingEndpointSecurityManager.h"
|
||||
#import "Source/santad/EventProviders/SNTDeviceManager.h"
|
||||
#import "Source/santad/EventProviders/SNTDriverManager.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityManager.h"
|
||||
#import "Source/santad/EventProviders/SNTEventProvider.h"
|
||||
#import "Source/santad/Logs/SNTFileEventLog.h"
|
||||
#import "Source/santad/Logs/SNTSyslogEventLog.h"
|
||||
#import "Source/santad/Logs/SNTEventLog.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
#import "Source/santad/SNTDaemonControlController.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
@@ -40,14 +42,15 @@
|
||||
#import "Source/santad/SNTSyncdQueue.h"
|
||||
|
||||
@interface SNTApplication ()
|
||||
@property DASessionRef diskArbSession;
|
||||
@property id<SNTEventProvider> eventProvider;
|
||||
@property SNTEventLog *eventLog;
|
||||
@property SNTExecutionController *execController;
|
||||
@property SNTCompilerController *compilerController;
|
||||
@property SNTDeviceManager *deviceManager;
|
||||
@property MOLXPCConnection *controlConnection;
|
||||
@property SNTNotificationQueue *notQueue;
|
||||
@property pid_t syncdPID;
|
||||
@property MOLXPCConnection *metricsConnection;
|
||||
@property dispatch_source_t metricsTimer;
|
||||
@end
|
||||
|
||||
@implementation SNTApplication
|
||||
@@ -89,9 +92,10 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
switch ([configurator eventLogType]) {
|
||||
case SNTEventLogTypeSyslog: _eventLog = [[SNTSyslogEventLog alloc] init]; break;
|
||||
case SNTEventLogTypeFilelog: _eventLog = [[SNTFileEventLog alloc] init]; break;
|
||||
_deviceManager = [[SNTDeviceManager alloc] init];
|
||||
self.deviceManager.blockUSBMount = [configurator blockUSBMount];
|
||||
if ([configurator remountUSBMode] != nil) {
|
||||
self.deviceManager.remountArgs = [configurator remountUSBMode];
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
|
||||
@@ -129,6 +133,23 @@
|
||||
forKeyPath:NSStringFromSelector(@selector(blockedPathRegex))
|
||||
options:bits
|
||||
context:NULL];
|
||||
[configurator addObserver:self
|
||||
forKeyPath:NSStringFromSelector(@selector(exportMetrics))
|
||||
options:bits
|
||||
context:NULL];
|
||||
[configurator addObserver:self
|
||||
forKeyPath:NSStringFromSelector(@selector(metricExportInterval))
|
||||
options:bits
|
||||
context:NULL];
|
||||
[configurator addObserver:self
|
||||
forKeyPath:NSStringFromSelector(@selector(blockUSBMount))
|
||||
options:bits
|
||||
context:NULL];
|
||||
[configurator addObserver:self
|
||||
forKeyPath:NSStringFromSelector(@selector(remountUSBMode))
|
||||
options:bits
|
||||
context:NULL];
|
||||
|
||||
if (![configurator enableSystemExtension]) {
|
||||
[configurator addObserver:self
|
||||
forKeyPath:NSStringFromSelector(@selector(enableSystemExtension))
|
||||
@@ -140,8 +161,7 @@
|
||||
SNTDaemonControlController *dc =
|
||||
[[SNTDaemonControlController alloc] initWithEventProvider:_eventProvider
|
||||
notificationQueue:self.notQueue
|
||||
syncdQueue:syncdQueue
|
||||
eventLog:_eventLog];
|
||||
syncdQueue:syncdQueue];
|
||||
|
||||
_controlConnection =
|
||||
[[MOLXPCConnection alloc] initServerWithName:[SNTXPCControlInterface serviceID]];
|
||||
@@ -152,20 +172,22 @@
|
||||
[_controlConnection resume];
|
||||
|
||||
// Initialize the transitive whitelisting controller object.
|
||||
_compilerController = [[SNTCompilerController alloc] initWithEventProvider:_eventProvider
|
||||
eventLog:_eventLog];
|
||||
_compilerController = [[SNTCompilerController alloc] initWithEventProvider:_eventProvider];
|
||||
|
||||
// Initialize the binary checker object
|
||||
_execController = [[SNTExecutionController alloc] initWithEventProvider:_eventProvider
|
||||
ruleTable:ruleTable
|
||||
eventTable:eventTable
|
||||
notifierQueue:self.notQueue
|
||||
syncdQueue:syncdQueue
|
||||
eventLog:_eventLog];
|
||||
syncdQueue:syncdQueue];
|
||||
// Start up santactl as a daemon if a sync server exists.
|
||||
[self startSyncd];
|
||||
|
||||
if (!_execController) return nil;
|
||||
|
||||
if ([configurator exportMetrics]) {
|
||||
[self startMetricsPoll];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -176,7 +198,7 @@
|
||||
|
||||
[self performSelectorInBackground:@selector(beginListeningForDecisionRequests) withObject:nil];
|
||||
[self performSelectorInBackground:@selector(beginListeningForLogRequests) withObject:nil];
|
||||
[self performSelectorInBackground:@selector(beginListeningForDiskMounts) withObject:nil];
|
||||
[self performSelectorInBackground:@selector(beginListeningForMountRequests) withObject:nil];
|
||||
}
|
||||
|
||||
- (void)beginListeningForDecisionRequests {
|
||||
@@ -216,59 +238,80 @@
|
||||
NSString *path = @(message.path);
|
||||
if (!path) break;
|
||||
if ([re numberOfMatchesInString:path options:0 range:NSMakeRange(0, path.length)]) {
|
||||
[self->_eventLog logFileModification:message];
|
||||
[[SNTEventLog logger] logFileModification:message];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ACTION_NOTIFY_EXEC: {
|
||||
[self->_eventLog logAllowedExecution:message];
|
||||
[[SNTEventLog logger] logAllowedExecution:message];
|
||||
break;
|
||||
}
|
||||
case ACTION_NOTIFY_FORK: [self->_eventLog logFork:message]; break;
|
||||
case ACTION_NOTIFY_EXIT: [self->_eventLog logExit:message]; break;
|
||||
case ACTION_NOTIFY_FORK: [[SNTEventLog logger] logFork:message]; break;
|
||||
case ACTION_NOTIFY_EXIT: [[SNTEventLog logger] logExit:message]; break;
|
||||
default: LOGE(@"Received log request without a valid action: %d", message.action); break;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)beginListeningForDiskMounts {
|
||||
dispatch_queue_t disk_queue =
|
||||
dispatch_queue_create("com.google.santad.disk_queue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_diskArbSession = DASessionCreate(NULL);
|
||||
DASessionSetDispatchQueue(_diskArbSession, disk_queue);
|
||||
|
||||
DARegisterDiskAppearedCallback(_diskArbSession, NULL, diskAppearedCallback,
|
||||
(__bridge void *)self);
|
||||
DARegisterDiskDescriptionChangedCallback(_diskArbSession, NULL, NULL,
|
||||
diskDescriptionChangedCallback, (__bridge void *)self);
|
||||
DARegisterDiskDisappearedCallback(_diskArbSession, NULL, diskDisappearedCallback,
|
||||
(__bridge void *)self);
|
||||
- (void)beginListeningForMountRequests {
|
||||
[self.deviceManager listen];
|
||||
}
|
||||
|
||||
void diskAppearedCallback(DADiskRef disk, void *context) {
|
||||
SNTApplication *app = (__bridge SNTApplication *)context;
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
// Taken from Apple's Concurrency Programming Guide.
|
||||
dispatch_source_t createDispatchTimer(uint64_t interval, uint64_t leeway, dispatch_queue_t queue,
|
||||
dispatch_block_t block) {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
|
||||
|
||||
[app.eventLog logDiskAppeared:props];
|
||||
if (timer) {
|
||||
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
|
||||
dispatch_source_set_event_handler(timer, block);
|
||||
dispatch_resume(timer);
|
||||
}
|
||||
|
||||
return timer;
|
||||
}
|
||||
|
||||
void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
|
||||
SNTApplication *app = (__bridge SNTApplication *)context;
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
/*
|
||||
* Create a SNTMetricSet instance and start reporting essential metrics immediately to the metric
|
||||
* service.
|
||||
*/
|
||||
- (void)startMetricsPoll {
|
||||
NSUInteger interval = [[SNTConfigurator configurator] metricExportInterval];
|
||||
|
||||
if (props[@"DAVolumePath"]) [app.eventLog logDiskAppeared:props];
|
||||
LOGI(@"starting to export metrics every %ld seconds", interval);
|
||||
void (^exportMetricsBlock)(void) = ^{
|
||||
[[self.metricsConnection remoteObjectProxy]
|
||||
exportForMonitoring:[[SNTMetricSet sharedInstance] export]];
|
||||
};
|
||||
|
||||
static dispatch_once_t registerMetrics;
|
||||
|
||||
dispatch_once(®isterMetrics, ^{
|
||||
_metricsConnection = [SNTXPCMetricServiceInterface configuredConnection];
|
||||
[_metricsConnection resume];
|
||||
|
||||
LOGD(@"registering core metrics");
|
||||
SNTRegisterCoreMetrics();
|
||||
exportMetricsBlock();
|
||||
});
|
||||
|
||||
dispatch_source_t timer = createDispatchTimer(interval * NSEC_PER_SEC, 1ull * NSEC_PER_SEC,
|
||||
dispatch_get_main_queue(), exportMetricsBlock);
|
||||
if (!timer) {
|
||||
LOGE(@"failed to created timer for exporting metrics");
|
||||
return;
|
||||
}
|
||||
|
||||
_metricsTimer = timer;
|
||||
}
|
||||
|
||||
void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
SNTApplication *app = (__bridge SNTApplication *)context;
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
- (void)stopMetricsPoll {
|
||||
if (!_metricsTimer) {
|
||||
LOGE(@"stopMetricsPoll called while _metricsTimer is nil");
|
||||
return;
|
||||
}
|
||||
|
||||
[app.eventLog logDiskDisappeared:props];
|
||||
[app.eventProvider flushCacheNonRootOnly:YES];
|
||||
dispatch_source_cancel(_metricsTimer);
|
||||
}
|
||||
|
||||
- (void)startSyncd {
|
||||
@@ -331,6 +374,47 @@ void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
LOGI(@"The penultimate exit.");
|
||||
exit(0);
|
||||
}
|
||||
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(exportMetrics))]) {
|
||||
BOOL new = [ change[newKey] boolValue ];
|
||||
BOOL old = [change[oldKey] boolValue];
|
||||
|
||||
if (old == NO && new == YES) {
|
||||
LOGI(@"metricsExport changed NO -> YES, starting to export metrics");
|
||||
[self startMetricsPoll];
|
||||
} else if (old == YES && new == NO) {
|
||||
LOGI(@"metricsExport changed YES -> NO, stopping export of metrics");
|
||||
[self stopMetricsPoll];
|
||||
}
|
||||
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(metricExportInterval))]) {
|
||||
// clang-format off
|
||||
NSUInteger new = [ change[newKey] unsignedIntegerValue ];
|
||||
NSUInteger old = [ change[oldKey] unsignedIntegerValue ];
|
||||
// clang-format on
|
||||
|
||||
LOGI(@"MetricExportInterval changed from %ld to %ld restarting export", old, new);
|
||||
|
||||
[self stopMetricsPoll];
|
||||
[self startMetricsPoll];
|
||||
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(blockUSBMount))]) {
|
||||
BOOL new = [ change[newKey] boolValue ];
|
||||
BOOL old = [change[oldKey] boolValue];
|
||||
|
||||
if (new != old) {
|
||||
LOGI(@"BlockUSBMount changed: %d -> %d", old, new);
|
||||
self.deviceManager.blockUSBMount = new;
|
||||
}
|
||||
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(remountUSBMode))]) {
|
||||
NSArray<NSString *> *new = [ change[newKey] isKindOfClass : [NSArray class] ]
|
||||
? (NSArray<NSString *> *)change[newKey]
|
||||
: nil;
|
||||
NSArray<NSString *> *old =
|
||||
[change[oldKey] isKindOfClass:[NSArray class]] ? (NSArray<NSString *> *)change[oldKey] : nil;
|
||||
|
||||
if (![old isEqualToArray:new]) {
|
||||
LOGI(@"RemountArgs changed: %s -> %s", [[old componentsJoinedByString:@","] UTF8String],
|
||||
[[new componentsJoinedByString:@","] UTF8String]);
|
||||
self.deviceManager.remountArgs = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
153
Source/santad/SNTApplicationBenchmark.m
Normal file
153
Source/santad/SNTApplicationBenchmark.m
Normal file
@@ -0,0 +1,153 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/santad/SNTApplication.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurityTestUtil.h"
|
||||
|
||||
@interface SNTApplicationBenchmark : XCTestCase
|
||||
@property id mockSNTDatabaseController;
|
||||
@property id mockConfigurator;
|
||||
@end
|
||||
|
||||
@implementation SNTApplicationBenchmark : XCTestCase
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
fclose(stdout);
|
||||
self.mockSNTDatabaseController = OCMClassMock([SNTDatabaseController class]);
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator enableSystemExtension]).andReturn(true);
|
||||
OCMStub([self.mockConfigurator enableSysxCache]).andReturn(false);
|
||||
}
|
||||
|
||||
+ (NSArray<XCTPerformanceMetric> *)defaultPerformanceMetrics {
|
||||
return @[
|
||||
XCTPerformanceMetric_WallClockTime,
|
||||
|
||||
// Metrics visible and controllable from the XCode UI but without a symbol exposed for them:
|
||||
@"com.apple.XCTPerformanceMetric_RunTime",
|
||||
@"com.apple.XCTPerformanceMetric_UserTime",
|
||||
@"com.apple.XCTPerformanceMetric_SystemTime",
|
||||
@"com.apple.XCTPerformanceMetric_HighWaterMarkForHeapAllocations",
|
||||
@"com.apple.XCTPerformanceMetric_PersistentHeapAllocations",
|
||||
@"com.apple.XCTPerformanceMetric_PersistentHeapAllocationsNodes",
|
||||
@"com.apple.XCTPerformanceMetric_PersistentVMAllocations",
|
||||
@"com.apple.XCTPerformanceMetric_TotalHeapAllocationsKilobytes",
|
||||
@"com.apple.XCTPerformanceMetric_TransientHeapAllocationsKilobytes",
|
||||
@"com.apple.XCTPerformanceMetric_TransientHeapAllocationsNodes",
|
||||
@"com.apple.XCTPerformanceMetric_TransientVMAllocationsKilobytes",
|
||||
@"com.apple.XCTPerformanceMetric_HighWaterMarkForVMAllocations",
|
||||
];
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockSNTDatabaseController stopMocking];
|
||||
[self.mockConfigurator stopMocking];
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void)executeAndMeasure:(NSString *)binaryName testPath:(NSString *)testPath {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
OCMStub([self.mockSNTDatabaseController databasePath]).andReturn(testPath);
|
||||
|
||||
SNTApplication *app = [[SNTApplication alloc] init];
|
||||
[app start];
|
||||
|
||||
// es events will start flowing in as soon as es_subscribe is called, regardless
|
||||
// of whether we're ready or not for it.
|
||||
XCTestExpectation *santaInit =
|
||||
[self expectationWithDescription:@"Wait for Santa to subscribe to EndpointSecurity"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
|
||||
while ([mockES.subscriptions[ES_EVENT_TYPE_AUTH_EXEC] isEqualTo:@NO])
|
||||
;
|
||||
|
||||
[santaInit fulfill];
|
||||
});
|
||||
|
||||
// Ugly hack to deflake the test and allow listenForDecisionRequests to install the correct
|
||||
// decision callback.
|
||||
[self waitForExpectations:@[ santaInit ] timeout:2.0];
|
||||
|
||||
// MeasureMetrics actually runs all of the individual events asynchronously at once.
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
|
||||
void (^executeBinary)(void) = ^void(void) {
|
||||
NSString *binaryPath = [NSString pathWithComponents:@[ testPath, binaryName ]];
|
||||
struct stat fileStat;
|
||||
lstat(binaryPath.UTF8String, &fileStat);
|
||||
|
||||
ESMessage *msg = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = binaryPath;
|
||||
m.executable->stat = fileStat;
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_EXEC;
|
||||
m.message->event = (es_events_t){.exec = {.target = m.process}};
|
||||
}];
|
||||
|
||||
__block BOOL complete = NO;
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_EXEC
|
||||
withCallback:^(ESResponse *r) {
|
||||
complete = YES;
|
||||
}];
|
||||
|
||||
[self startMeasuring];
|
||||
[mockES triggerHandler:msg.message];
|
||||
while (!complete)
|
||||
;
|
||||
[self stopMeasuring];
|
||||
dispatch_semaphore_signal(sem);
|
||||
};
|
||||
|
||||
[self measureMetrics:[SNTApplicationBenchmark defaultPerformanceMetrics]
|
||||
automaticallyStartMeasuring:false
|
||||
forBlock:executeBinary];
|
||||
|
||||
int sampleSize = 10;
|
||||
|
||||
for (size_t i = 0; i < sampleSize; i++) {
|
||||
dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_SEC));
|
||||
}
|
||||
}
|
||||
|
||||
// Microbenchmarking analysis of binary execution
|
||||
- (void)testMeasureExecutionDeny {
|
||||
NSString *testPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
NSString *fullTestPath = [NSString pathWithComponents:@[
|
||||
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testPath
|
||||
]];
|
||||
|
||||
[self executeAndMeasure:@"badbinary" testPath:fullTestPath];
|
||||
}
|
||||
|
||||
- (void)testMeasureExecutionAllow {
|
||||
NSString *testPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
NSString *fullTestPath = [NSString pathWithComponents:@[
|
||||
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testPath
|
||||
]];
|
||||
|
||||
[self executeAndMeasure:@"goodbinary" testPath:fullTestPath];
|
||||
}
|
||||
|
||||
@end
|
||||
17
Source/santad/SNTApplicationCoreMetrics.h
Normal file
17
Source/santad/SNTApplicationCoreMetrics.h
Normal file
@@ -0,0 +1,17 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
|
||||
void SNTRegisterCoreMetrics();
|
||||
142
Source/santad/SNTApplicationCoreMetrics.m
Normal file
142
Source/santad/SNTApplicationCoreMetrics.m
Normal file
@@ -0,0 +1,142 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#import "Source/common/SNTSystemInfo.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <mach/mach.h>
|
||||
#include <sys/resource.h>
|
||||
|
||||
/**
|
||||
* Register the mode metric checking the config before reporting the status.
|
||||
*/
|
||||
static void RegisterModeMetric(SNTMetricSet *metricSet) {
|
||||
SNTMetricStringGauge *mode = [metricSet stringGaugeWithName:@"/santa/mode"
|
||||
fieldNames:@[]
|
||||
helpText:@"Santa's operating mode"];
|
||||
|
||||
// create a callback that gets the current mode
|
||||
[metricSet registerCallback:^{
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
switch (config.clientMode) {
|
||||
case SNTClientModeLockdown: [mode set:@"lockdown" forFieldValues:@[]]; break;
|
||||
case SNTClientModeMonitor: [mode set:@"monitor" forFieldValues:@[]]; break;
|
||||
default:
|
||||
// Should never be reached.
|
||||
[mode set:@"unknown" forFieldValues:@[]];
|
||||
break;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register metrics for measuring memory usage.
|
||||
*/
|
||||
static void RegisterMemoryAndCPUMetrics(SNTMetricSet *metricSet) {
|
||||
SNTMetricInt64Gauge *vsize =
|
||||
[metricSet int64GaugeWithName:@"/proc/memory/virtual_size"
|
||||
fieldNames:@[]
|
||||
helpText:@"The virtual memory size of this process"];
|
||||
SNTMetricInt64Gauge *rsize =
|
||||
[metricSet int64GaugeWithName:@"/proc/memory/resident_size"
|
||||
fieldNames:@[]
|
||||
helpText:@"The resident set size of this process"];
|
||||
|
||||
SNTMetricDoubleGauge *cpuUsage =
|
||||
[metricSet doubleGaugeWithName:@"/proc/cpu_usage"
|
||||
fieldNames:@[ @"mode" ] // "user" or "system"
|
||||
helpText:@"CPU time consumed by this process, in seconds"];
|
||||
|
||||
[metricSet registerCallback:^(void) {
|
||||
struct mach_task_basic_info info;
|
||||
mach_msg_type_number_t size = MACH_TASK_BASIC_INFO_COUNT;
|
||||
kern_return_t ret =
|
||||
task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &size);
|
||||
|
||||
if (ret != KERN_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
[vsize set:info.virtual_size forFieldValues:@[]];
|
||||
[rsize set:info.resident_size forFieldValues:@[]];
|
||||
|
||||
// convert times to seconds
|
||||
double user_time = info.user_time.seconds + (info.user_time.microseconds / 1e6);
|
||||
double system_time = info.system_time.seconds + (info.system_time.microseconds / 1e6);
|
||||
|
||||
[cpuUsage set:user_time forFieldValues:@[ @"user" ]];
|
||||
[cpuUsage set:system_time forFieldValues:@[ @"system" ]];
|
||||
}];
|
||||
}
|
||||
|
||||
static void RegisterHostnameAndUsernameLabels(SNTMetricSet *metricSet) {
|
||||
NSString *hostname = [NSProcessInfo processInfo].hostName;
|
||||
|
||||
[metricSet addRootLabel:@"host_name" value:hostname];
|
||||
[metricSet addRootLabel:@"username" value:NSUserName()];
|
||||
[metricSet addRootLabel:@"job_name" value:@"santad"];
|
||||
[metricSet addRootLabel:@"service_name" value:@"santa"];
|
||||
|
||||
// get extra root labels from configuration
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
NSDictionary *extraLabels = [config extraMetricLabels];
|
||||
|
||||
if (extraLabels.count == 0) return;
|
||||
|
||||
for (NSString *key in extraLabels) {
|
||||
// remove the root label if the value is empty.
|
||||
if ([@"" isEqualToString:(NSString *)extraLabels[key]]) {
|
||||
[metricSet removeRootLabel:key];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set or override the value.
|
||||
[metricSet addRootLabel:key value:(NSString *)extraLabels[key]];
|
||||
}
|
||||
}
|
||||
static void RegisterCommonSantaMetrics(SNTMetricSet *metricSet) {
|
||||
NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
||||
|
||||
// register the version
|
||||
[metricSet addConstantStringWithName:@"/build/label"
|
||||
helpText:@"Version of the binary"
|
||||
value:version];
|
||||
|
||||
// register start time
|
||||
[metricSet
|
||||
addConstantIntegerWithName:@"/proc/birth_timestamp"
|
||||
helpText:@"Start time of Santad, in microseconds since epoch"
|
||||
value:(long long)([[NSDate date] timeIntervalSince1970] * 1000000)];
|
||||
|
||||
// Register OS version
|
||||
[metricSet addConstantStringWithName:@"/proc/os/version"
|
||||
helpText:@"Short operating System version"
|
||||
value:[SNTSystemInfo osVersion]];
|
||||
|
||||
RegisterModeMetric(metricSet);
|
||||
// TODO(markowsky) Register CSR status
|
||||
// TODO(markowsky) Register system extension status
|
||||
}
|
||||
|
||||
void SNTRegisterCoreMetrics() {
|
||||
SNTMetricSet *metricSet = [SNTMetricSet sharedInstance];
|
||||
RegisterHostnameAndUsernameLabels(metricSet);
|
||||
RegisterMemoryAndCPUMetrics(metricSet);
|
||||
RegisterCommonSantaMetrics(metricSet);
|
||||
}
|
||||
269
Source/santad/SNTApplicationCoreMetricsTest.m
Normal file
269
Source/santad/SNTApplicationCoreMetricsTest.m
Normal file
@@ -0,0 +1,269 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#import "Source/common/SNTSystemInfo.h"
|
||||
#import "Source/santad/SNTApplicationCoreMetrics.h"
|
||||
#import "Source/santametricservice/Formats/SNTMetricFormatTestHelper.h"
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
@interface SNTApplicationCoreMetricsTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property NSDictionary *extraMetricLabels;
|
||||
@end
|
||||
|
||||
@implementation SNTApplicationCoreMetricsTest
|
||||
|
||||
- (void)setUp {
|
||||
self.extraMetricLabels = @{@"service_name" : @"santa", @"corp_site" : @"roam"};
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockConfigurator stopMocking];
|
||||
}
|
||||
|
||||
- (NSDictionary *)fixUpDatesAndDataValuesOf:(NSDictionary *)exportedMetrics {
|
||||
NSMutableDictionary *mutableMetrics =
|
||||
[[SNTMetricFormatTestHelper convertDatesToFixedDateWithExportDict:exportedMetrics] mutableCopy];
|
||||
|
||||
// Ensure that we got data of the correct type but do not check the values as they'll change per
|
||||
// call. Instead we stub out the values for testing.
|
||||
NSMutableDictionary *metric = mutableMetrics[@"metrics"][@"/proc/birth_timestamp"];
|
||||
XCTAssertTrue([metric[@"fields"][@""][0][@"data"] isKindOfClass:[NSNumber class]],
|
||||
@"/proc/birth_timestamp data is not a number");
|
||||
metric[@"fields"][@""][0][@"data"] = @1634148013203157;
|
||||
mutableMetrics[@"metrics"][@"/proc/birth_timestamp"] = metric;
|
||||
|
||||
// Fix up CPU usage
|
||||
metric = mutableMetrics[@"metrics"][@"/proc/cpu_usage"];
|
||||
XCTAssertTrue([metric[@"fields"][@"mode"][0][@"data"] isKindOfClass:[@0.63002 class]],
|
||||
@"/proc/cpu_usage has non-floating point data");
|
||||
metric[@"fields"][@"mode"][0][@"data"] = @0.63002;
|
||||
XCTAssertTrue([metric[@"fields"][@"mode"][1][@"data"] isKindOfClass:[@0.29522 class]],
|
||||
@"/proc/cpu_usage has non-floating point data");
|
||||
metric[@"fields"][@"mode"][1][@"data"] = @0.29522;
|
||||
metric = mutableMetrics[@"metrics"][@"/proc/cpu_usage"];
|
||||
|
||||
// Fix up Memory (resident size)
|
||||
metric = mutableMetrics[@"metrics"][@"/proc/memory/resident_size"];
|
||||
XCTAssertTrue([metric[@"fields"][@""][0][@"data"] isKindOfClass:[@22097920 class]]);
|
||||
metric[@"fields"][@""][0][@"data"] = @22097920;
|
||||
mutableMetrics[@"metrics"][@"/proc/memory/resident_size"] = metric;
|
||||
|
||||
// Fix up Memory (virtual size)
|
||||
metric = mutableMetrics[@"metrics"][@"/proc/memory/virtual_size"];
|
||||
XCTAssertTrue([metric[@"fields"][@""][0][@"data"] isKindOfClass:[@35634683904 class]]);
|
||||
metric[@"fields"][@""][0][@"data"] = @35634683904;
|
||||
mutableMetrics[@"metrics"][@"/proc/memory/virtual_size"] = metric;
|
||||
|
||||
return mutableMetrics;
|
||||
}
|
||||
|
||||
- (void)testRegisteringCoreMetrics {
|
||||
OCMStub([self.mockConfigurator extraMetricLabels]).andReturn(self.extraMetricLabels);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
|
||||
SNTRegisterCoreMetrics();
|
||||
|
||||
NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
||||
|
||||
NSString *shortOSVersion = [SNTSystemInfo osVersion];
|
||||
|
||||
NSISO8601DateFormatter *formatter = [[NSISO8601DateFormatter alloc] init];
|
||||
|
||||
if (@available(macOS 10.13, *)) {
|
||||
formatter.formatOptions =
|
||||
NSISO8601DateFormatWithFractionalSeconds | NSISO8601DateFormatWithInternetDateTime;
|
||||
}
|
||||
|
||||
NSDate *fixedDate = [formatter dateFromString:@"2021-09-16T21:07:34.826Z"];
|
||||
NSString *hostname = [NSProcessInfo processInfo].hostName;
|
||||
|
||||
NSDictionary *expected = @{
|
||||
@"metrics" : @{
|
||||
@"/build/label" : @{
|
||||
@"description" : @"Version of the binary",
|
||||
@"type" : @2,
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@"created" : fixedDate,
|
||||
@"data" : version,
|
||||
@"last_updated" : fixedDate,
|
||||
@"value" : @""
|
||||
} ]
|
||||
},
|
||||
},
|
||||
@"/proc/birth_timestamp" : @{
|
||||
@"description" : @"Start time of Santad, in microseconds since epoch",
|
||||
@"type" : @3,
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@"created" : fixedDate,
|
||||
@"data" : @1634148013203157,
|
||||
@"last_updated" : fixedDate,
|
||||
@"value" : @""
|
||||
} ],
|
||||
},
|
||||
},
|
||||
@"/proc/cpu_usage" : @{
|
||||
@"description" : @"CPU time consumed by this process, in seconds",
|
||||
@"type" : @8,
|
||||
@"fields" : @{
|
||||
@"mode" : @[
|
||||
@{
|
||||
@"created" : fixedDate,
|
||||
@"data" : @0.63002,
|
||||
@"last_updated" : fixedDate,
|
||||
@"value" : @"user"
|
||||
},
|
||||
@{
|
||||
@"created" : fixedDate,
|
||||
@"data" : @0.29522,
|
||||
@"last_updated" : fixedDate,
|
||||
@"value" : @"system"
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
@"/proc/memory/resident_size" : @{
|
||||
@"description" : @"The resident set size of this process",
|
||||
@"type" : @7,
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@"created" : fixedDate,
|
||||
@"data" : @22097920,
|
||||
@"last_updated" : fixedDate,
|
||||
@"value" : @""
|
||||
} ],
|
||||
},
|
||||
},
|
||||
@"/proc/memory/virtual_size" : @{
|
||||
@"description" : @"The virtual memory size of this process",
|
||||
@"type" : @7,
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@"created" : fixedDate,
|
||||
@"data" : @35634683904,
|
||||
@"last_updated" : fixedDate,
|
||||
@"value" : @""
|
||||
} ],
|
||||
},
|
||||
},
|
||||
@"/proc/os/version" : @{
|
||||
@"description" : @"Short operating System version",
|
||||
@"type" : @2,
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@"created" : fixedDate,
|
||||
@"data" : shortOSVersion,
|
||||
@"last_updated" : fixedDate,
|
||||
@"value" : @""
|
||||
} ],
|
||||
},
|
||||
},
|
||||
@"/santa/mode" : @{
|
||||
@"description" : @"Santa's operating mode",
|
||||
@"type" : @6,
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@"created" : fixedDate,
|
||||
@"data" : @"lockdown",
|
||||
@"last_updated" : fixedDate,
|
||||
@"value" : @""
|
||||
} ],
|
||||
},
|
||||
},
|
||||
},
|
||||
@"root_labels" : @{
|
||||
@"host_name" : hostname,
|
||||
@"job_name" : @"santad",
|
||||
@"service_name" : @"santa",
|
||||
@"corp_site" : @"roam",
|
||||
@"username" : [NSProcessInfo processInfo].userName
|
||||
},
|
||||
};
|
||||
|
||||
SNTMetricSet *metricSet = [SNTMetricSet sharedInstance];
|
||||
|
||||
NSDictionary *exportedMetrics = [self fixUpDatesAndDataValuesOf:[metricSet export]];
|
||||
XCTAssertNotNil(exportedMetrics);
|
||||
|
||||
XCTAssertEqualObjects(expected[@"root_labels"], exportedMetrics[@"root_labels"],
|
||||
@"root_labels are different");
|
||||
|
||||
for (NSString *metricName in expected[@"metrics"]) {
|
||||
NSDictionary *other = exportedMetrics[@"metrics"][metricName];
|
||||
XCTAssertNotNil(other, @"exported Metrics Missing %@", metricName);
|
||||
XCTAssertEqualObjects(expected[@"metrics"][metricName], other,
|
||||
@"%@ does not match expected values", metricName);
|
||||
}
|
||||
}
|
||||
|
||||
// Test that setting a new value for an existing rootLabel overwrites the labels value.
|
||||
//
|
||||
// Becareful modifiying this test as the singleton makes this order dependent.
|
||||
//
|
||||
- (void)testRootLabelReplacement {
|
||||
self.extraMetricLabels = @{@"host_name" : @"santa-host"};
|
||||
OCMStub([self.mockConfigurator extraMetricLabels]).andReturn(self.extraMetricLabels);
|
||||
|
||||
SNTRegisterCoreMetrics();
|
||||
|
||||
SNTMetricSet *metricSet = [SNTMetricSet sharedInstance];
|
||||
NSDictionary *output = [metricSet export];
|
||||
|
||||
NSDictionary *expectedRootLabels = @{
|
||||
@"host_name" : @"santa-host",
|
||||
@"job_name" : @"santad",
|
||||
@"service_name" : @"santa",
|
||||
@"corp_site" : @"roam",
|
||||
@"username" : [NSProcessInfo processInfo].userName
|
||||
};
|
||||
|
||||
XCTAssertEqualObjects(expectedRootLabels, output[@"root_labels"],
|
||||
@"failed to update host_name root label");
|
||||
}
|
||||
|
||||
// Test that setting a rootLabel key to "" removes it from the root labels.
|
||||
//
|
||||
// Becareful modifiying this test as the singleton makes this order dependent.
|
||||
//
|
||||
- (void)testRootLabelRemoval {
|
||||
self.extraMetricLabels = @{@"host_name" : @""};
|
||||
OCMStub([self.mockConfigurator extraMetricLabels]).andReturn(self.extraMetricLabels);
|
||||
|
||||
SNTRegisterCoreMetrics();
|
||||
|
||||
SNTMetricSet *metricSet = [SNTMetricSet sharedInstance];
|
||||
NSDictionary *output = [metricSet export];
|
||||
|
||||
NSDictionary *expectedRootLabels = @{
|
||||
@"job_name" : @"santad",
|
||||
@"service_name" : @"santa",
|
||||
@"corp_site" : @"roam",
|
||||
@"username" : [NSProcessInfo processInfo].userName,
|
||||
};
|
||||
|
||||
XCTAssertEqualObjects(expectedRootLabels, output[@"root_labels"],
|
||||
@"failed to remove only host_name root label");
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -69,10 +69,11 @@
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"Wait for santa's Auth dispatch queue"];
|
||||
__block ESResponse *got = nil;
|
||||
[mockES registerResponseCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_EXEC
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
NSString *binaryPath = [NSString pathWithComponents:@[ testPath, binaryName ]];
|
||||
struct stat fileStat;
|
||||
@@ -93,31 +94,16 @@
|
||||
testPath, binaryName);
|
||||
}
|
||||
|
||||
- (void)testBinaryRules {
|
||||
- (void)testRules {
|
||||
NSString *testPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
NSDictionary *testCases = @{
|
||||
@"badbinary" : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
@"goodbinary" : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
|
||||
@"noop" : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
|
||||
|
||||
};
|
||||
NSString *fullTestPath = [NSString pathWithComponents:@[
|
||||
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testPath
|
||||
]];
|
||||
|
||||
for (NSString *binary in testCases) {
|
||||
[self checkBinaryExecution:binary
|
||||
testPath:fullTestPath
|
||||
wantResult:[testCases[binary] intValue]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testCertRules {
|
||||
NSString *testPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
NSDictionary *testCases = @{
|
||||
@"banned_teamid" : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
@"banned_teamid_allowed_binary" : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
|
||||
@"badcert" : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
@"goodcert" : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
|
||||
@"noop" : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
|
||||
};
|
||||
NSString *fullTestPath = [NSString pathWithComponents:@[
|
||||
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testPath
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
// Designated initializer takes a SNTEventLog instance so that we can
|
||||
// call saveDecisionDetails: to create a fake cached decision for transitive
|
||||
// rule creation requests that are still pending.
|
||||
- (instancetype)initWithEventProvider:(SNTDriverManager *)driverManager
|
||||
eventLog:(SNTEventLog *)eventLog;
|
||||
- (instancetype)initWithEventProvider:(SNTDriverManager *)driverManager;
|
||||
|
||||
// Whenever an executable file is closed or renamed whitelist the resulting file.
|
||||
// We assume that we have already determined that the writing process was a compiler.
|
||||
|
||||
@@ -27,17 +27,14 @@
|
||||
|
||||
@interface SNTCompilerController ()
|
||||
@property id<SNTEventProvider> eventProvider;
|
||||
@property SNTEventLog *eventLog;
|
||||
@end
|
||||
|
||||
@implementation SNTCompilerController
|
||||
|
||||
- (instancetype)initWithEventProvider:(id<SNTEventProvider>)eventProvider
|
||||
eventLog:(SNTEventLog *)eventLog {
|
||||
- (instancetype)initWithEventProvider:(id<SNTEventProvider>)eventProvider {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_eventProvider = eventProvider;
|
||||
_eventLog = eventLog;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -50,11 +47,11 @@
|
||||
cd.decision = SNTEventStateAllowPendingTransitive;
|
||||
cd.vnodeId = message.vnode_id;
|
||||
cd.sha256 = @"pending";
|
||||
[self.eventLog cacheDecision:cd];
|
||||
[[SNTEventLog logger] cacheDecision:cd];
|
||||
}
|
||||
|
||||
- (void)removeFakeDecision:(santa_message_t)message {
|
||||
[self.eventLog forgetCachedDecisionForVnodeId:message.vnode_id];
|
||||
[[SNTEventLog logger] forgetCachedDecisionForVnodeId:message.vnode_id];
|
||||
}
|
||||
|
||||
// Assume that this method is called only when we already know that the writing process is a
|
||||
@@ -71,20 +68,20 @@
|
||||
// Check if there is an existing (non-transitive) rule for this file. We leave existing rules
|
||||
// alone, so that a allowlist or blocklist rule can't be overwritten by a transitive one.
|
||||
SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable];
|
||||
SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256 certificateSHA256:nil];
|
||||
SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256 certificateSHA256:nil teamID:nil];
|
||||
if (!prevRule || prevRule.state == SNTRuleStateAllowTransitive) {
|
||||
// Construct a new transitive allowlist rule for the executable.
|
||||
SNTRule *rule = [[SNTRule alloc] initWithShasum:fi.SHA256
|
||||
state:SNTRuleStateAllowTransitive
|
||||
type:SNTRuleTypeBinary
|
||||
customMsg:@""];
|
||||
SNTRule *rule = [[SNTRule alloc] initWithIdentifier:fi.SHA256
|
||||
state:SNTRuleStateAllowTransitive
|
||||
type:SNTRuleTypeBinary
|
||||
customMsg:@""];
|
||||
|
||||
// Add the new rule to the rules database.
|
||||
NSError *err;
|
||||
if (![ruleTable addRules:@[ rule ] cleanSlate:NO error:&err]) {
|
||||
LOGE(@"unable to add new transitive rule to database: %@", err.localizedDescription);
|
||||
} else {
|
||||
[self.eventLog
|
||||
[[SNTEventLog logger]
|
||||
writeLog:[NSString
|
||||
stringWithFormat:@"action=ALLOWLIST|pid=%d|pidversion=%d|path=%s|sha256=%@",
|
||||
message.pid, message.pidversion, target, fi.SHA256]];
|
||||
|
||||
@@ -28,6 +28,5 @@
|
||||
|
||||
- (instancetype)initWithEventProvider:(SNTDriverManager *)driverManager
|
||||
notificationQueue:(SNTNotificationQueue *)notQueue
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue
|
||||
eventLog:(SNTEventLog *)eventLog;
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue;
|
||||
@end
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTStrengthify.h"
|
||||
@@ -44,7 +45,6 @@ double watchdogRAMPeak = 0;
|
||||
@interface SNTDaemonControlController ()
|
||||
@property NSString *_syncXsrfToken;
|
||||
@property SNTPolicyProcessor *policyProcessor;
|
||||
@property SNTEventLog *eventLog;
|
||||
@property id<SNTEventProvider> eventProvider;
|
||||
@property SNTNotificationQueue *notQueue;
|
||||
@property SNTSyncdQueue *syncdQueue;
|
||||
@@ -54,8 +54,7 @@ double watchdogRAMPeak = 0;
|
||||
|
||||
- (instancetype)initWithEventProvider:(id<SNTEventProvider>)eventProvider
|
||||
notificationQueue:(SNTNotificationQueue *)notQueue
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue
|
||||
eventLog:(SNTEventLog *)eventLog {
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_policyProcessor =
|
||||
@@ -63,7 +62,6 @@ double watchdogRAMPeak = 0;
|
||||
_eventProvider = eventProvider;
|
||||
_notQueue = notQueue;
|
||||
_syncdQueue = syncdQueue;
|
||||
_eventLog = eventLog;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -94,10 +92,10 @@ double watchdogRAMPeak = 0;
|
||||
#pragma mark Database ops
|
||||
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive))reply {
|
||||
int64_t transitive, int64_t teamID))reply {
|
||||
SNTRuleTable *rdb = [SNTDatabaseController ruleTable];
|
||||
reply([rdb binaryRuleCount], [rdb certificateRuleCount], [rdb compilerRuleCount],
|
||||
[rdb transitiveRuleCount]);
|
||||
[rdb transitiveRuleCount], [rdb teamIDRuleCount]);
|
||||
}
|
||||
|
||||
- (void)databaseRuleAddRules:(NSArray *)rules
|
||||
@@ -139,9 +137,11 @@ double watchdogRAMPeak = 0;
|
||||
|
||||
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
reply:(void (^)(SNTRule *))reply {
|
||||
reply([[SNTDatabaseController ruleTable] ruleForBinarySHA256:binarySHA256
|
||||
certificateSHA256:certificateSHA256]);
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID]);
|
||||
}
|
||||
|
||||
#pragma mark Decision Ops
|
||||
@@ -149,10 +149,12 @@ double watchdogRAMPeak = 0;
|
||||
- (void)decisionForFilePath:(NSString *)filePath
|
||||
fileSHA256:(NSString *)fileSHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
reply:(void (^)(SNTEventState))reply {
|
||||
reply([self.policyProcessor decisionForFilePath:filePath
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256]
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID]
|
||||
.decision);
|
||||
}
|
||||
|
||||
@@ -223,6 +225,15 @@ double watchdogRAMPeak = 0;
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)setBlockUSBMount:(BOOL)enabled reply:(void (^)(void))reply {
|
||||
[[SNTConfigurator configurator] setBlockUSBMount:enabled];
|
||||
reply();
|
||||
}
|
||||
- (void)setRemountUSBMode:(NSArray *)remountUSBMode reply:(void (^)(void))reply {
|
||||
[[SNTConfigurator configurator] setRemountUSBMode:remountUSBMode];
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)enableBundles:(void (^)(BOOL))reply {
|
||||
reply([SNTConfigurator configurator].enableBundles);
|
||||
}
|
||||
@@ -241,6 +252,13 @@ double watchdogRAMPeak = 0;
|
||||
reply();
|
||||
}
|
||||
|
||||
#pragma mark Metrics Ops
|
||||
|
||||
- (void)metrics:(void (^)(NSDictionary *))reply {
|
||||
SNTMetricSet *metricSet = [SNTMetricSet sharedInstance];
|
||||
reply([metricSet export]);
|
||||
}
|
||||
|
||||
#pragma mark GUI Ops
|
||||
|
||||
- (void)setNotificationListener:(NSXPCListenerEndpoint *)listener {
|
||||
@@ -301,7 +319,7 @@ double watchdogRAMPeak = 0;
|
||||
[eventTable addStoredEvent:event];
|
||||
|
||||
// Log all of the generated bundle events.
|
||||
[self.eventLog logBundleHashingEvents:events];
|
||||
[[SNTEventLog logger] logBundleHashingEvents:events];
|
||||
|
||||
WEAKIFY(self);
|
||||
|
||||
|
||||
@@ -18,6 +18,23 @@
|
||||
#include "Source/common/SNTKernelCommon.h"
|
||||
#include "Source/santad/EventProviders/SNTEventProvider.h"
|
||||
|
||||
const static NSString *kBlockBinary = @"BlockBinary";
|
||||
const static NSString *kAllowBinary = @"AllowBinary";
|
||||
const static NSString *kBlockCertificate = @"BlockCertificate";
|
||||
const static NSString *kAllowCertificate = @"AllowCertificate";
|
||||
const static NSString *kBlockTeamID = @"BlockTeamID";
|
||||
const static NSString *kAllowTeamID = @"AllowTeamID";
|
||||
const static NSString *kBlockScope = @"BlockScope";
|
||||
const static NSString *kAllowScope = @"AllowScope";
|
||||
const static NSString *kAllowUnknown = @"AllowUnknown";
|
||||
const static NSString *kBlockUnknown = @"BlockUnknown";
|
||||
const static NSString *kAllowCompiler = @"AllowCompiler";
|
||||
const static NSString *kAllowTransitive = @"AllowTransitive";
|
||||
const static NSString *kUnknownEventState = @"Unknown";
|
||||
const static NSString *kBlockPrinterWorkaround = @"BlockPrinterWorkaround";
|
||||
const static NSString *kAllowNoFileInfo = @"AllowNoFileInfo";
|
||||
const static NSString *kAllowNullVNode = @"AllowNullVNode";
|
||||
|
||||
@class MOLCodesignChecker;
|
||||
@class SNTDriverManager;
|
||||
@class SNTEventLog;
|
||||
@@ -40,8 +57,7 @@
|
||||
ruleTable:(SNTRuleTable *)ruleTable
|
||||
eventTable:(SNTEventTable *)eventTable
|
||||
notifierQueue:(SNTNotificationQueue *)notifierQueue
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue
|
||||
eventLog:(SNTEventLog *)eventLog;
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue;
|
||||
|
||||
///
|
||||
/// Handles the logic of deciding whether to allow the binary to run or not, sends the response to
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <utmpx.h>
|
||||
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#include "Source/common/SNTMetricSet.h"
|
||||
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
|
||||
@@ -46,26 +47,33 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
|
||||
@interface SNTExecutionController ()
|
||||
@property id<SNTEventProvider> eventProvider;
|
||||
@property SNTEventLog *eventLog;
|
||||
@property SNTEventTable *eventTable;
|
||||
@property SNTNotificationQueue *notifierQueue;
|
||||
@property SNTPolicyProcessor *policyProcessor;
|
||||
@property SNTRuleTable *ruleTable;
|
||||
@property SNTSyncdQueue *syncdQueue;
|
||||
@property SNTMetricCounter *events;
|
||||
|
||||
@property dispatch_queue_t eventQueue;
|
||||
@end
|
||||
|
||||
@implementation SNTExecutionController
|
||||
|
||||
static NSString *const kPrinterProxyPreMonterey =
|
||||
(@"/System/Library/Frameworks/Carbon.framework/Versions/Current/"
|
||||
@"Frameworks/Print.framework/Versions/Current/Plugins/PrinterProxy.app/"
|
||||
@"Contents/MacOS/PrinterProxy");
|
||||
static NSString *const kPrinterProxyPostMonterey =
|
||||
(@"/System/Library/PrivateFrameworks/PrintingPrivate.framework/"
|
||||
@"Versions/Current/Plugins/PrinterProxy.app/Contents/MacOS/PrinterProxy");
|
||||
|
||||
#pragma mark Initializers
|
||||
|
||||
- (instancetype)initWithEventProvider:(id<SNTEventProvider>)eventProvider
|
||||
ruleTable:(SNTRuleTable *)ruleTable
|
||||
eventTable:(SNTEventTable *)eventTable
|
||||
notifierQueue:(SNTNotificationQueue *)notifierQueue
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue
|
||||
eventLog:(SNTEventLog *)eventLog {
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_eventProvider = eventProvider;
|
||||
@@ -73,7 +81,6 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
_eventTable = eventTable;
|
||||
_notifierQueue = notifierQueue;
|
||||
_syncdQueue = syncdQueue;
|
||||
_eventLog = eventLog;
|
||||
_policyProcessor = [[SNTPolicyProcessor alloc] initWithRuleTable:_ruleTable];
|
||||
|
||||
_eventQueue = dispatch_queue_create("com.google.santad.event_upload", DISPATCH_QUEUE_SERIAL);
|
||||
@@ -81,10 +88,37 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
// This establishes the XPC connection between libsecurity and syspolicyd.
|
||||
// Not doing this causes a deadlock as establishing this link goes through xpcproxy.
|
||||
(void)[[MOLCodesignChecker alloc] initWithSelf];
|
||||
|
||||
SNTMetricSet *metricSet = [SNTMetricSet sharedInstance];
|
||||
_events = [metricSet counterWithName:@"/santa/events"
|
||||
fieldNames:@[ @"action_response" ]
|
||||
helpText:@"Events processed by Santa per response"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)incrementEventCounters:(SNTEventState)eventType {
|
||||
const NSString *eventTypeStr;
|
||||
|
||||
switch (eventType) {
|
||||
case SNTEventStateBlockBinary: eventTypeStr = kBlockBinary; break;
|
||||
case SNTEventStateAllowBinary: eventTypeStr = kAllowBinary; break;
|
||||
case SNTEventStateBlockCertificate: eventTypeStr = kBlockCertificate; break;
|
||||
case SNTEventStateAllowCertificate: eventTypeStr = kAllowCertificate; break;
|
||||
case SNTEventStateBlockTeamID: eventTypeStr = kBlockTeamID; break;
|
||||
case SNTEventStateAllowTeamID: eventTypeStr = kAllowTeamID; break;
|
||||
case SNTEventStateBlockScope: eventTypeStr = kBlockScope; break;
|
||||
case SNTEventStateAllowScope: eventTypeStr = kAllowScope; break;
|
||||
case SNTEventStateBlockUnknown: eventTypeStr = kBlockUnknown; break;
|
||||
case SNTEventStateAllowUnknown: eventTypeStr = kAllowUnknown; break;
|
||||
case SNTEventStateAllowCompiler: eventTypeStr = kAllowCompiler; break;
|
||||
case SNTEventStateAllowTransitive: eventTypeStr = kAllowTransitive; break;
|
||||
default: eventTypeStr = kUnknownEventState; break;
|
||||
}
|
||||
|
||||
[_events incrementForFieldValues:@[ (NSString *)eventTypeStr ]];
|
||||
}
|
||||
|
||||
#pragma mark Binary Validation
|
||||
|
||||
- (void)validateBinaryWithMessage:(santa_message_t)message {
|
||||
@@ -92,19 +126,23 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
if (unlikely(message.path == NULL)) {
|
||||
LOGE(@"Path for vnode_id is NULL: %llu/%llu", message.vnode_id.fsid, message.vnode_id.fileid);
|
||||
[self.eventProvider postAction:ACTION_RESPOND_ALLOW forMessage:message];
|
||||
[self.events incrementForFieldValues:@[ (NSString *)kAllowNullVNode ]];
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *fileInfoError;
|
||||
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:@(message.path) error:&fileInfoError];
|
||||
if (unlikely(!binInfo)) {
|
||||
LOGE(@"Failed to read file %@: %@", @(message.path), fileInfoError.localizedDescription);
|
||||
[self.eventProvider postAction:ACTION_RESPOND_ALLOW forMessage:message];
|
||||
[self.events incrementForFieldValues:@[ (NSString *)kAllowNoFileInfo ]];
|
||||
return;
|
||||
}
|
||||
|
||||
// PrinterProxy workaround, see description above the method for more details.
|
||||
if ([self printerProxyWorkaround:binInfo]) {
|
||||
[self.eventProvider postAction:ACTION_RESPOND_DENY forMessage:message];
|
||||
[self.events incrementForFieldValues:@[ (NSString *)kBlockPrinterWorkaround ]];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,6 +151,7 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
LOGD(@"%@ is larger than %zu. Letting santa-driver know we are working on it.", binInfo.path,
|
||||
kLargeBinarySize);
|
||||
[self.eventProvider postAction:ACTION_RESPOND_ACK forMessage:message];
|
||||
// TODO(markowsky): Maybe add a metric here for how many large executables we're seeing.
|
||||
}
|
||||
|
||||
SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo];
|
||||
@@ -127,7 +166,7 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
// ACTION_NOTIFY_EXEC message related to the transitive rule is received.
|
||||
NSString *ttyPath;
|
||||
if (action == ACTION_RESPOND_ALLOW) {
|
||||
[_eventLog cacheDecision:cd];
|
||||
[[SNTEventLog logger] cacheDecision:cd];
|
||||
} else {
|
||||
ttyPath = [self ttyPathForPID:message.ppid];
|
||||
}
|
||||
@@ -141,10 +180,13 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
// Send the decision to the kernel.
|
||||
[self.eventProvider postAction:action forMessage:message];
|
||||
|
||||
// Increment counters;
|
||||
[self incrementEventCounters:cd.decision];
|
||||
|
||||
// Log to database if necessary.
|
||||
if (cd.decision != SNTEventStateAllowBinary && cd.decision != SNTEventStateAllowCompiler &&
|
||||
cd.decision != SNTEventStateAllowTransitive && cd.decision != SNTEventStateAllowCertificate &&
|
||||
cd.decision != SNTEventStateAllowScope) {
|
||||
cd.decision != SNTEventStateAllowTeamID && cd.decision != SNTEventStateAllowScope) {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
se.occurrenceDate = [[NSDate alloc] init];
|
||||
se.fileSHA256 = cd.sha256;
|
||||
@@ -181,13 +223,16 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
se.quarantineTimestamp = binInfo.quarantineTimestamp;
|
||||
se.quarantineAgentBundleID = binInfo.quarantineAgentBundleID;
|
||||
|
||||
dispatch_async(_eventQueue, ^{
|
||||
[self.eventTable addStoredEvent:se];
|
||||
});
|
||||
// Only store events if there is a sync server configured.
|
||||
if ([SNTConfigurator configurator].syncBaseURL) {
|
||||
dispatch_async(_eventQueue, ^{
|
||||
[self.eventTable addStoredEvent:se];
|
||||
});
|
||||
}
|
||||
|
||||
// If binary was blocked, do the needful
|
||||
if (action != ACTION_RESPOND_ALLOW && action != ACTION_RESPOND_ALLOW_COMPILER) {
|
||||
[_eventLog logDeniedExecution:cd withMessage:message];
|
||||
[[SNTEventLog logger] logDeniedExecution:cd withMessage:message];
|
||||
|
||||
if ([[SNTConfigurator configurator] enableBundles] && binInfo.bundle) {
|
||||
// If the binary is part of a bundle, find and hash all the related binaries in the bundle.
|
||||
@@ -245,13 +290,10 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
- (BOOL)printerProxyWorkaround:(SNTFileInfo *)fi {
|
||||
if ([fi.path hasSuffix:@"/Contents/MacOS/PrinterProxy"] &&
|
||||
[fi.path containsString:@"Library/Printers"]) {
|
||||
NSString *proxyPath = (@"/System/Library/Frameworks/Carbon.framework/Versions/Current/"
|
||||
@"Frameworks/Print.framework/Versions/Current/Plugins/PrinterProxy.app/"
|
||||
@"Contents/MacOS/PrinterProxy");
|
||||
SNTFileInfo *proxyFi = [[SNTFileInfo alloc] initWithPath:proxyPath];
|
||||
SNTFileInfo *proxyFi = [self printerProxyFileInfo];
|
||||
if ([proxyFi.SHA256 isEqual:fi.SHA256]) return NO;
|
||||
|
||||
NSFileHandle *inFh = [NSFileHandle fileHandleForReadingAtPath:proxyPath];
|
||||
NSFileHandle *inFh = [NSFileHandle fileHandleForReadingAtPath:proxyFi.path];
|
||||
NSFileHandle *outFh = [NSFileHandle fileHandleForWritingAtPath:fi.path];
|
||||
[outFh writeData:[inFh readDataToEndOfFile]];
|
||||
[inFh closeFile];
|
||||
@@ -266,6 +308,15 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
return NO;
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an SNTFileInfo for the system PrinterProxy path on this system.
|
||||
*/
|
||||
- (SNTFileInfo *)printerProxyFileInfo {
|
||||
SNTFileInfo *proxyInfo = [[SNTFileInfo alloc] initWithPath:kPrinterProxyPostMonterey];
|
||||
if (!proxyInfo) proxyInfo = [[SNTFileInfo alloc] initWithPath:kPrinterProxyPreMonterey];
|
||||
return proxyInfo;
|
||||
}
|
||||
|
||||
- (NSString *)ttyPathForPID:(pid_t)pid {
|
||||
if (pid < 2) return nil; // don't bother even looking for launchd.
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/santad/DataLayer/SNTEventTable.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
@@ -52,6 +53,8 @@
|
||||
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
NSURL *url = [NSURL URLWithString:@"https://localhost/test"];
|
||||
OCMStub([self.mockConfigurator syncBaseURL]).andReturn(url);
|
||||
|
||||
self.mockDriverManager = OCMClassMock([SNTDriverManager class]);
|
||||
|
||||
@@ -69,8 +72,7 @@
|
||||
ruleTable:self.mockRuleDatabase
|
||||
eventTable:self.mockEventDatabase
|
||||
notifierQueue:nil
|
||||
syncdQueue:nil
|
||||
eventLog:nil];
|
||||
syncdQueue:nil];
|
||||
}
|
||||
|
||||
/// Return a pre-configured santa_message_ t for testing with.
|
||||
@@ -87,14 +89,23 @@
|
||||
return (santa_vnode_id_t){.fsid = 1234, .fileid = 5678};
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockFileInfo stopMocking];
|
||||
[self.mockCodesignChecker stopMocking];
|
||||
[self.mockDriverManager stopMocking];
|
||||
[self.mockRuleDatabase stopMocking];
|
||||
[self.mockEventDatabase stopMocking];
|
||||
- (void)checkMetricCounters:(const NSString *)expectedFieldValueName
|
||||
expected:(NSNumber *)expectedValue {
|
||||
SNTMetricSet *metricSet = [SNTMetricSet sharedInstance];
|
||||
NSDictionary *eventCounter = [metricSet export][@"metrics"][@"/santa/events"];
|
||||
BOOL foundField;
|
||||
for (NSDictionary *fieldValue in eventCounter[@"fields"][@"action_response"]) {
|
||||
if ([expectedFieldValueName isEqualToString:fieldValue[@"value"]]) {
|
||||
XCTAssertEqualObjects(expectedValue, fieldValue[@"data"],
|
||||
@"%@ counter does not match expected value", expectedFieldValueName);
|
||||
foundField = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[super tearDown];
|
||||
if (!foundField) {
|
||||
XCTFail(@"failed to find %@ field value", expectedFieldValueName);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testBinaryAllowRule {
|
||||
@@ -104,11 +115,13 @@
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil]).andReturn(rule);
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]);
|
||||
[self checkMetricCounters:@"AllowBinary" expected:@2];
|
||||
}
|
||||
|
||||
- (void)testBinaryBlockRule {
|
||||
@@ -118,11 +131,15 @@
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil]).andReturn(rule);
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_DENY forMessage:[self getMessage]]);
|
||||
|
||||
// verify that we're incrementing the binary block
|
||||
[self checkMetricCounters:@"BlockBinary" expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCertificateAllowRule {
|
||||
@@ -135,11 +152,13 @@
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeCertificate;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil certificateSHA256:@"a"]).andReturn(rule);
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]);
|
||||
[self checkMetricCounters:kAllowCertificate expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCertificateBlockRule {
|
||||
@@ -152,11 +171,16 @@
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeCertificate;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil certificateSHA256:@"a"]).andReturn(rule);
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_DENY forMessage:[self getMessage]]);
|
||||
OCMVerifyAllWithDelay(self.mockEventDatabase, 1);
|
||||
[self checkMetricCounters:@"BlockCertificate" expected:@1];
|
||||
}
|
||||
|
||||
- (void)testBinaryAllowCompilerRule {
|
||||
@@ -167,12 +191,14 @@
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil]).andReturn(rule);
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_ALLOW_COMPILER
|
||||
forMessage:[self getMessage]]);
|
||||
[self checkMetricCounters:kAllowCompiler expected:@1];
|
||||
}
|
||||
|
||||
- (void)testBinaryAllowCompilerRuleDisabled {
|
||||
@@ -183,11 +209,13 @@
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil]).andReturn(rule);
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]);
|
||||
[self checkMetricCounters:kAllowBinary expected:@1];
|
||||
}
|
||||
|
||||
- (void)testBinaryAllowTransitiveRule {
|
||||
@@ -198,11 +226,14 @@
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil]).andReturn(rule);
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]);
|
||||
|
||||
[self checkMetricCounters:@"AllowBinary" expected:@2];
|
||||
}
|
||||
|
||||
- (void)testBinaryAllowTransitiveRuleDisabled {
|
||||
@@ -214,11 +245,17 @@
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil]).andReturn(rule);
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_DENY forMessage:[self getMessage]]);
|
||||
OCMVerifyAllWithDelay(self.mockEventDatabase, 1);
|
||||
[self checkMetricCounters:kAllowBinary expected:@2];
|
||||
[self checkMetricCounters:kAllowTransitive expected:@1];
|
||||
}
|
||||
|
||||
- (void)testDefaultDecision {
|
||||
@@ -226,33 +263,42 @@
|
||||
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
|
||||
OCMExpect([self.mockConfigurator clientMode]).andReturn(SNTClientModeMonitor);
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]);
|
||||
|
||||
OCMExpect([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_DENY forMessage:[self getMessage]]);
|
||||
}
|
||||
OCMVerifyAllWithDelay(self.mockEventDatabase, 1);
|
||||
|
||||
- (void)testOutOfScope {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(NO);
|
||||
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]);
|
||||
[self checkMetricCounters:kBlockUnknown expected:@2];
|
||||
[self checkMetricCounters:kAllowUnknown expected:@1];
|
||||
}
|
||||
|
||||
- (void)testMissingShasum {
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]);
|
||||
[self checkMetricCounters:kAllowScope expected:@1];
|
||||
}
|
||||
|
||||
- (void)testOutOfScope {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(NO);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]);
|
||||
[self checkMetricCounters:kAllowScope expected:@2];
|
||||
}
|
||||
|
||||
- (void)testPageZero {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
|
||||
OCMStub([self.mockFileInfo isMissingPageZero]).andReturn(YES);
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
[self.sut validateBinaryWithMessage:[self getMessage]];
|
||||
OCMVerify([self.mockDriverManager postAction:ACTION_RESPOND_DENY forMessage:[self getMessage]]);
|
||||
OCMVerifyAllWithDelay(self.mockEventDatabase, 1);
|
||||
[self checkMetricCounters:kBlockUnknown expected:@3];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
///
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256;
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID;
|
||||
|
||||
/// Convenience initializer with nil hashes for both the file and certificate.
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo;
|
||||
@@ -57,6 +58,7 @@
|
||||
///
|
||||
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256;
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID;
|
||||
|
||||
@end
|
||||
|
||||
@@ -40,9 +40,11 @@
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256 {
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.sha256 = fileSHA256 ?: fileInfo.SHA256;
|
||||
cd.teamID = teamID;
|
||||
|
||||
// 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.
|
||||
@@ -64,11 +66,15 @@
|
||||
cd.certSHA256 = csInfo.leafCertificate.SHA256;
|
||||
cd.certCommonName = csInfo.leafCertificate.commonName;
|
||||
cd.certChain = csInfo.certificates;
|
||||
cd.teamID = teamID ?: [csInfo.signingInformation valueForKey:@"teamid"];
|
||||
teamID = cd.teamID;
|
||||
}
|
||||
}
|
||||
cd.quarantineURL = fileInfo.quarantineDataURL;
|
||||
|
||||
SNTRule *rule = [self.ruleTable ruleForBinarySHA256:cd.sha256 certificateSHA256:cd.certSHA256];
|
||||
SNTRule *rule = [self.ruleTable ruleForBinarySHA256:cd.sha256
|
||||
certificateSHA256:cd.certSHA256
|
||||
teamID:teamID];
|
||||
if (rule) {
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeBinary:
|
||||
@@ -115,6 +121,20 @@
|
||||
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.decision = SNTEventStateBlockTeamID;
|
||||
return cd;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -149,19 +169,22 @@
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo {
|
||||
return [self decisionForFileInfo:fileInfo fileSHA256:nil certificateSHA256:nil];
|
||||
return [self decisionForFileInfo:fileInfo fileSHA256:nil certificateSHA256:nil teamID:nil];
|
||||
}
|
||||
|
||||
// Used by `$ santactl fileinfo`.
|
||||
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256 {
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID {
|
||||
SNTFileInfo *fileInfo;
|
||||
NSError *error;
|
||||
fileInfo = [[SNTFileInfo alloc] initWithPath:filePath error:&error];
|
||||
if (!fileInfo) LOGW(@"Failed to read file %@: %@", filePath, error.localizedDescription);
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256];
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID];
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.application-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)com.google.santa.daemon</string>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>EQHXZ8M8AV</string>
|
||||
<key>com.apple.application-identifier</key>
|
||||
<string>EQHXZ8M8AV.com.google.santa.daemon</string>
|
||||
<key>com.apple.developer.endpoint-security.client</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>EQHXZ8M8AV</string>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>EQHXZ8M8AV.com.google.santa.daemon</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
5
Source/santad/testdata/binaryrules/banned_teamid.c
vendored
Normal file
5
Source/santad/testdata/binaryrules/banned_teamid.c
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
#include <stdio.h>
|
||||
int main(int argc, char* argv[]) {
|
||||
printf("This binary should fail to execute because of its banned team ID!");
|
||||
return 0;
|
||||
}
|
||||
BIN
Source/santad/testdata/binaryrules/banned_teamid_allowed_binary
vendored
Executable file
BIN
Source/santad/testdata/binaryrules/banned_teamid_allowed_binary
vendored
Executable file
Binary file not shown.
8
Source/santad/testdata/binaryrules/banned_teamid_allowed_binary.c
vendored
Normal file
8
Source/santad/testdata/binaryrules/banned_teamid_allowed_binary.c
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
#include <stdio.h>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
printf(
|
||||
"Hello world, this binary's execution is okay even though its teamID "
|
||||
"isn't");
|
||||
return 0;
|
||||
}
|
||||
BIN
Source/santad/testdata/binaryrules/goodcert
vendored
BIN
Source/santad/testdata/binaryrules/goodcert
vendored
Binary file not shown.
BIN
Source/santad/testdata/binaryrules/rules.db
vendored
BIN
Source/santad/testdata/binaryrules/rules.db
vendored
Binary file not shown.
@@ -14,11 +14,14 @@ objc_library(
|
||||
],
|
||||
deps = [
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDropRootPrivs",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTXPCMetricServiceInterface",
|
||||
"//Source/santametricservice/Formats:SNTMetricMonarchJSONFormat",
|
||||
"//Source/santametricservice/Formats:SNTMetricRawJSONFormat",
|
||||
"//Source/santametricservice/Writers:SNTMetricFileWriter",
|
||||
"//Source/santametricservice/Writers:SNTMetricHTTPWriter",
|
||||
"@MOLCodesignChecker",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
@@ -27,6 +30,7 @@ objc_library(
|
||||
santa_unit_test(
|
||||
name = "SNTMetricServiceTest",
|
||||
srcs = ["SNTMetricServiceTest.m"],
|
||||
structured_resources = ["//Source/santametricservice/Formats:testdata"],
|
||||
deps = [
|
||||
":SNTMetricServiceLib",
|
||||
"//Source/santametricservice/Formats:SNTMetricFormatTestHelper",
|
||||
@@ -38,8 +42,8 @@ test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":SNTMetricServiceTest",
|
||||
"//Source/santametricservice/Formats:SNTMetricRawJSONFormatTest",
|
||||
"//Source/santametricservice/Writers:SNTMetricFileWriterTest",
|
||||
"//Source/santametricservice/Formats:format_tests",
|
||||
"//Source/santametricservice/Writers:writer_tests",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -53,7 +57,13 @@ macos_command_line_application(
|
||||
],
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "10.15",
|
||||
provisioning_profile = select({
|
||||
"//:ci_build": None,
|
||||
"//conditions:default": "Santa_Dev.provisionprofile",
|
||||
}),
|
||||
version = "//:version",
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [":SNTMetricServiceLib"],
|
||||
deps = [
|
||||
":SNTMetricServiceLib",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -33,6 +33,20 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTMetricMonarchJSONFormat",
|
||||
srcs = [
|
||||
"SNTMetricFormat.h",
|
||||
"SNTMetricMonarchJSONFormat.h",
|
||||
"SNTMetricMonarchJSONFormat.m",
|
||||
],
|
||||
deps = [
|
||||
":SNTMetricFormat",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTMetricRawJSONFormatTest",
|
||||
srcs = [
|
||||
@@ -45,9 +59,27 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTMetricMonarchJSONFormatTest",
|
||||
srcs = [
|
||||
"SNTMetricMonarchJSONFormatTest.m",
|
||||
],
|
||||
structured_resources = [":testdata"],
|
||||
deps = [
|
||||
":SNTMetricFormatTestHelper",
|
||||
":SNTMetricMonarchJSONFormat",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "testdata",
|
||||
srcs = glob(["testdata/**"]),
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "format_tests",
|
||||
tests = [
|
||||
":SNTMetricMonarchJSONFormatTest",
|
||||
":SNTMetricRawJSONFormatTest",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface SNTMetricFormatTestHelper : NSObject
|
||||
+ (NSDictionary *)convertDatesToFixedDateWithExportDict:(NSDictionary *)exportDict;
|
||||
+ (NSDictionary *)createValidMetricsDictionary;
|
||||
@end
|
||||
|
||||
@@ -19,8 +19,22 @@
|
||||
|
||||
@implementation SNTMetricFormatTestHelper
|
||||
+ (NSDictionary *)convertDatesToFixedDateWithExportDict:(NSMutableDictionary *)exportDict {
|
||||
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
||||
[formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
|
||||
id formatter;
|
||||
|
||||
if (@available(macOS 10.13, *)) {
|
||||
NSISO8601DateFormatter *isoFormatter = [[NSISO8601DateFormatter alloc] init];
|
||||
|
||||
isoFormatter.formatOptions =
|
||||
NSISO8601DateFormatWithInternetDateTime | NSISO8601DateFormatWithFractionalSeconds;
|
||||
formatter = isoFormatter;
|
||||
} else {
|
||||
NSDateFormatter *localFormatter = [[NSDateFormatter alloc] init];
|
||||
localFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
|
||||
localFormatter.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601];
|
||||
localFormatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
|
||||
formatter = localFormatter;
|
||||
}
|
||||
|
||||
NSDate *fixedDate = [formatter dateFromString:@"2021-09-16T21:07:34.826Z"];
|
||||
|
||||
for (NSString *metricName in exportDict[@"metrics"]) {
|
||||
@@ -45,26 +59,26 @@
|
||||
|
||||
// Add constants
|
||||
[metricSet addConstantStringWithName:@"/build/label"
|
||||
helpText:@"Software version running."
|
||||
helpText:@"Software version running"
|
||||
value:@"20210809.0.1"];
|
||||
[metricSet addConstantBooleanWithName:@"/santa/using_endpoint_security_framework"
|
||||
helpText:@"Is santad using the endpoint security framework."
|
||||
helpText:@"Is santad using the endpoint security framework"
|
||||
value:YES];
|
||||
[metricSet addConstantIntegerWithName:@"/proc/birth_timestamp"
|
||||
helpText:@"Start time of this LogDumper instance, in microseconds "
|
||||
@"since epoch"
|
||||
value:(long long)(0x12345668910)];
|
||||
[metricSet
|
||||
addConstantIntegerWithName:@"/proc/birth_timestamp"
|
||||
helpText:@"Start time of this santad instance, in microseconds since epoch"
|
||||
value:(long long)(0x12345668910)];
|
||||
// Add Metrics
|
||||
SNTMetricCounter *c = [metricSet counterWithName:@"/santa/events"
|
||||
fieldNames:@[ @"rule_type" ]
|
||||
helpText:@"Count of events on the host"];
|
||||
helpText:@"Count of process exec events on the host"];
|
||||
|
||||
[c incrementForFieldValues:@[ @"binary" ]];
|
||||
[c incrementBy:2 forFieldValues:@[ @"certificate" ]];
|
||||
|
||||
SNTMetricInt64Gauge *g = [metricSet int64GaugeWithName:@"/santa/rules"
|
||||
fieldNames:@[ @"rule_type" ]
|
||||
helpText:@"Number of rules."];
|
||||
helpText:@"Number of rules"];
|
||||
|
||||
[g set:1 forFieldValues:@[ @"binary" ]];
|
||||
[g set:3 forFieldValues:@[ @"certificate" ]];
|
||||
@@ -73,12 +87,12 @@
|
||||
SNTMetricInt64Gauge *virtualMemoryGauge =
|
||||
[metricSet int64GaugeWithName:@"/proc/memory/virtual_size"
|
||||
fieldNames:@[]
|
||||
helpText:@"The virtual memory size of this process."];
|
||||
helpText:@"The virtual memory size of this process"];
|
||||
|
||||
SNTMetricInt64Gauge *residentMemoryGauge =
|
||||
[metricSet int64GaugeWithName:@"/proc/memory/resident_size"
|
||||
fieldNames:@[]
|
||||
helpText:@"The resident set siz of this process."];
|
||||
helpText:@"The resident set size of this process"];
|
||||
|
||||
[metricSet registerCallback:^(void) {
|
||||
[virtualMemoryGauge set:987654321 forFieldValues:@[]];
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/santametricservice/Formats/SNTMetricFormat.h"
|
||||
|
||||
@interface SNTMetricMonarchJSONFormat : NSObject <SNTMetricFormat>
|
||||
@end
|
||||
226
Source/santametricservice/Formats/SNTMetricMonarchJSONFormat.m
Normal file
226
Source/santametricservice/Formats/SNTMetricMonarchJSONFormat.m
Normal file
@@ -0,0 +1,226 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santametricservice/Formats/SNTMetricMonarchJSONFormat.h"
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
|
||||
const NSString *kMetricsCollection = @"metricsCollection";
|
||||
const NSString *kMetricsDataSet = @"metricsDataSet";
|
||||
const NSString *kMetricName = @"metricName";
|
||||
const NSString *kStreamKind = @"streamKind";
|
||||
const NSString *kValueType = @"valueType";
|
||||
const NSString *kDescription = @"description";
|
||||
const NSString *kData = @"data";
|
||||
const NSString *kFieldDescriptor = @"fieldDescriptor";
|
||||
const NSString *kBoolValue = @"boolValue";
|
||||
const NSString *kBoolValueType = @"BOOL";
|
||||
const NSString *kInt64Value = @"int64Value";
|
||||
const NSString *kInt64ValueType = @"INT64";
|
||||
const NSString *kStringValue = @"stringValue";
|
||||
const NSString *kStringValueType = @"STRING";
|
||||
const NSString *kName = @"name";
|
||||
const NSString *kStartTimestamp = @"startTimestamp";
|
||||
const NSString *kEndTimestamp = @"endTimestamp";
|
||||
const NSString *kRootLabels = @"rootLabels";
|
||||
const NSString *kKey = @"key";
|
||||
|
||||
@implementation SNTMetricMonarchJSONFormat {
|
||||
NSISO8601DateFormatter *_dateFormatter;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_dateFormatter = [[NSISO8601DateFormatter alloc] init];
|
||||
_dateFormatter.formatOptions = NSISO8601DateFormatWithInternetDateTime;
|
||||
|
||||
if (@available(macOS 10.13, *)) {
|
||||
_dateFormatter.formatOptions |= NSISO8601DateFormatWithFractionalSeconds;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeValueAndStreamKindFor:(NSString *)metricName
|
||||
withMetric:(NSDictionary *)metric
|
||||
into:(NSMutableDictionary *)monarchMetric {
|
||||
if (!metric[@"type"]) {
|
||||
LOGE(@"metric type not supposed to be nil for %@", metricName);
|
||||
return;
|
||||
}
|
||||
|
||||
NSNumber *type = metric[@"type"];
|
||||
if (![type isKindOfClass:[NSNumber class]]) {
|
||||
LOGE(@"%@ [@\"type\"] is not a number", metricName);
|
||||
return;
|
||||
}
|
||||
|
||||
switch ((SNTMetricType)[type intValue]) {
|
||||
case SNTMetricTypeConstantBool: monarchMetric[kValueType] = kBoolValueType; break;
|
||||
case SNTMetricTypeConstantString: monarchMetric[kValueType] = kStringValueType; break;
|
||||
case SNTMetricTypeConstantInt64: monarchMetric[kValueType] = kInt64ValueType; break;
|
||||
case SNTMetricTypeConstantDouble: monarchMetric[kValueType] = @"DOUBLE"; break;
|
||||
case SNTMetricTypeGaugeBool:
|
||||
monarchMetric[kStreamKind] = @"GAUGE";
|
||||
monarchMetric[kValueType] = kBoolValueType;
|
||||
break;
|
||||
case SNTMetricTypeGaugeString:
|
||||
monarchMetric[kStreamKind] = @"GAUGE";
|
||||
monarchMetric[kValueType] = kStringValueType;
|
||||
break;
|
||||
case SNTMetricTypeGaugeInt64:
|
||||
monarchMetric[kStreamKind] = @"GAUGE";
|
||||
monarchMetric[kValueType] = kInt64ValueType;
|
||||
break;
|
||||
case SNTMetricTypeGaugeDouble:
|
||||
monarchMetric[kStreamKind] = @"GAUGE";
|
||||
monarchMetric[kValueType] = @"DOUBLE";
|
||||
break;
|
||||
case SNTMetricTypeCounter:
|
||||
monarchMetric[kStreamKind] = @"CUMULATIVE";
|
||||
monarchMetric[kValueType] = kInt64ValueType;
|
||||
break;
|
||||
default:
|
||||
LOGE(@"encountered unknown SNTMetricType - %ld for %@", (SNTMetricType)metric[@"type"],
|
||||
metricName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<NSDictionary *> *)encodeDataForMetric:(NSDictionary *)metric {
|
||||
NSMutableArray<NSDictionary *> *monarchMetricData = [[NSMutableArray alloc] init];
|
||||
|
||||
for (NSString *fieldName in metric[@"fields"]) {
|
||||
for (NSDictionary *entry in metric[@"fields"][fieldName]) {
|
||||
NSMutableDictionary *monarchDataEntry = [[NSMutableDictionary alloc] init];
|
||||
|
||||
if (![fieldName isEqualToString:@""]) {
|
||||
monarchDataEntry[@"field"] = @[ @{kName : fieldName, kStringValue : entry[@"value"]} ];
|
||||
}
|
||||
|
||||
monarchDataEntry[kStartTimestamp] = [self->_dateFormatter stringFromDate:entry[@"created"]];
|
||||
monarchDataEntry[kEndTimestamp] =
|
||||
[self->_dateFormatter stringFromDate:entry[@"last_updated"]];
|
||||
|
||||
if (!metric[@"type"]) {
|
||||
LOGE(@"metric type is nil");
|
||||
continue;
|
||||
}
|
||||
|
||||
NSNumber *type = metric[@"type"];
|
||||
|
||||
switch ((SNTMetricType)[type intValue]) {
|
||||
case SNTMetricTypeConstantBool:
|
||||
case SNTMetricTypeGaugeBool: monarchDataEntry[kBoolValue] = entry[@"data"]; break;
|
||||
case SNTMetricTypeConstantInt64:
|
||||
case SNTMetricTypeGaugeInt64:
|
||||
case SNTMetricTypeCounter: monarchDataEntry[kInt64Value] = entry[@"data"]; break;
|
||||
case SNTMetricTypeConstantDouble:
|
||||
case SNTMetricTypeGaugeDouble: monarchDataEntry[@"doubleValue"] = entry[@"data"]; break;
|
||||
case SNTMetricTypeConstantString:
|
||||
case SNTMetricTypeGaugeString: monarchDataEntry[kStringValue] = entry[@"data"]; break;
|
||||
default: LOGE(@"encountered unknown SNTMetricType %ld", [type longValue]); break;
|
||||
}
|
||||
[monarchMetricData addObject:monarchDataEntry];
|
||||
}
|
||||
}
|
||||
|
||||
return monarchMetricData;
|
||||
}
|
||||
|
||||
/*
|
||||
* Translates SNTMetricSet fields to monarch's expected format. In this implementation only string
|
||||
* type fields are supported.
|
||||
*/
|
||||
- (NSArray<NSDictionary *> *)encodeFieldsFor:(NSDictionary *)metric {
|
||||
NSMutableArray<NSDictionary *> *monarchFields = [[NSMutableArray alloc] init];
|
||||
|
||||
for (NSString *fieldName in metric[@"fields"]) {
|
||||
if (![fieldName isEqualToString:@""]) {
|
||||
[monarchFields addObject:@{kName : fieldName, @"fieldType" : kStringValueType}];
|
||||
}
|
||||
}
|
||||
return monarchFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* formatMetric translates the SNTMetricSet metric entries into those consumable by Monarch.
|
||||
**/
|
||||
|
||||
- (NSDictionary *)formatMetric:(NSString *)name withMetric:(NSDictionary *)metric {
|
||||
NSMutableDictionary *monarchMetric = [[NSMutableDictionary alloc] init];
|
||||
|
||||
monarchMetric[kMetricName] = name;
|
||||
|
||||
if (metric[kDescription]) {
|
||||
monarchMetric[kDescription] = metric[kDescription];
|
||||
}
|
||||
|
||||
NSArray<NSDictionary *> *fieldDescriptorEntries = [self encodeFieldsFor:metric];
|
||||
if (fieldDescriptorEntries.count > 0) {
|
||||
monarchMetric[kFieldDescriptor] = [self encodeFieldsFor:metric];
|
||||
}
|
||||
|
||||
[self encodeValueAndStreamKindFor:name withMetric:metric into:monarchMetric];
|
||||
monarchMetric[@"data"] = [self encodeDataForMetric:metric];
|
||||
|
||||
return monarchMetric;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the metrics dictionary for exporting to JSON
|
||||
**/
|
||||
- (NSDictionary *)normalize:(NSDictionary *)metrics {
|
||||
NSMutableArray<NSDictionary *> *monarchMetrics = [[NSMutableArray alloc] init];
|
||||
|
||||
for (NSString *metricName in metrics[@"metrics"]) {
|
||||
[monarchMetrics addObject:[self formatMetric:metricName
|
||||
withMetric:metrics[@"metrics"][metricName]]];
|
||||
}
|
||||
|
||||
NSMutableArray<NSDictionary *> *rootLabels = [[NSMutableArray alloc] init];
|
||||
|
||||
for (NSString *keyName in metrics[@"root_labels"]) {
|
||||
[rootLabels addObject:@{kKey : keyName, kStringValue : metrics[@"root_labels"][keyName]}];
|
||||
}
|
||||
|
||||
return @{kMetricsCollection : @[ @{kMetricsDataSet : monarchMetrics, kRootLabels : rootLabels} ]};
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert normalizes and converts the metrics dictionary to a JSON
|
||||
* object consumable by parts of Google Monarch's tooling.
|
||||
*
|
||||
* @param metrics an NSDictionary exported by the SNTMetricSet
|
||||
* @param error a pointer to an NSError to allow errors to bubble up.
|
||||
*
|
||||
* Returns an NSArray containing one entry of all metrics serialized to JSON or
|
||||
* nil on error.
|
||||
*/
|
||||
- (NSArray<NSData *> *)convert:(NSDictionary *)metrics error:(NSError **)err {
|
||||
NSDictionary *normalizedMetrics = [self normalize:metrics];
|
||||
|
||||
NSData *json = [NSJSONSerialization dataWithJSONObject:normalizedMetrics
|
||||
options:NSJSONWritingPrettyPrinted
|
||||
error:err];
|
||||
|
||||
if (json == nil || (err != nil && *err != nil)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return @[ json ];
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,50 @@
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/santametricservice/Formats/SNTMetricFormatTestHelper.h"
|
||||
#import "Source/santametricservice/Formats/SNTMetricMonarchJSONFormat.h"
|
||||
|
||||
@interface SNTMetricMonarchJSONFormatTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTMetricMonarchJSONFormatTest
|
||||
|
||||
- (void)testMetricsConversionToJSON {
|
||||
NSDictionary *validMetricsDict = [SNTMetricFormatTestHelper createValidMetricsDictionary];
|
||||
SNTMetricMonarchJSONFormat *formatter = [[SNTMetricMonarchJSONFormat alloc] init];
|
||||
NSError *err = nil;
|
||||
NSArray<NSData *> *output = [formatter convert:validMetricsDict error:&err];
|
||||
|
||||
XCTAssertEqual(1, output.count);
|
||||
XCTAssertNotNil(output[0]);
|
||||
XCTAssertNil(err);
|
||||
|
||||
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:output[0]
|
||||
options:NSJSONReadingAllowFragments
|
||||
error:&err];
|
||||
XCTAssertNotNil(jsonDict);
|
||||
|
||||
NSString *path = [[NSBundle bundleForClass:[self class]] resourcePath];
|
||||
path = [path stringByAppendingPathComponent:@"testdata/json/monarch.json"];
|
||||
|
||||
NSData *goldenFileData = [NSData dataWithContentsOfFile:path];
|
||||
|
||||
XCTAssertNotNil(goldenFileData, @"unable to open / read golden file");
|
||||
|
||||
NSDictionary *expectedJSONDict =
|
||||
[NSJSONSerialization JSONObjectWithData:goldenFileData
|
||||
options:NSJSONReadingAllowFragments
|
||||
error:&err];
|
||||
|
||||
XCTAssertNotNil(expectedJSONDict);
|
||||
XCTAssertEqualObjects(expectedJSONDict, jsonDict, @"generated JSON does not match golden file.");
|
||||
}
|
||||
|
||||
- (void)testPassingANilOrNullErrorDoesNotCrash {
|
||||
SNTMetricMonarchJSONFormat *formatter = [[SNTMetricMonarchJSONFormat alloc] init];
|
||||
NSDictionary *validMetricsDict = [SNTMetricFormatTestHelper createValidMetricsDictionary];
|
||||
|
||||
[formatter convert:validMetricsDict error:nil];
|
||||
[formatter convert:validMetricsDict error:NULL];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -23,7 +23,9 @@
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_dateFormatter = [[NSDateFormatter alloc] init];
|
||||
[_dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
|
||||
_dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
|
||||
_dateFormatter.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601];
|
||||
_dateFormatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -81,7 +83,7 @@
|
||||
if (![NSJSONSerialization isValidJSONObject:normalizedMetrics]) {
|
||||
if (err != nil) {
|
||||
*err = [[NSError alloc]
|
||||
initWithDomain:@"SNTMetricRawJSONFileWriter"
|
||||
initWithDomain:@"com.google.santa.metricservice.formats.rawjson"
|
||||
code:EINVAL
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"unable to convert metrics to JSON: invalid metrics"
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
SNTMetricRawJSONFormat *formatter = [[SNTMetricRawJSONFormat alloc] init];
|
||||
NSDictionary *validMetricsDict = [SNTMetricFormatTestHelper createValidMetricsDictionary];
|
||||
|
||||
NSArray<NSData *> *output = [formatter convert:validMetricsDict error:nil];
|
||||
output = [formatter convert:validMetricsDict error:NULL];
|
||||
[formatter convert:validMetricsDict error:nil];
|
||||
[formatter convert:validMetricsDict error:NULL];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
152
Source/santametricservice/Formats/testdata/json/monarch.json
vendored
Normal file
152
Source/santametricservice/Formats/testdata/json/monarch.json
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"metricsCollection" : [
|
||||
{
|
||||
"rootLabels" : [
|
||||
{
|
||||
"key" : "hostname",
|
||||
"stringValue" : "testHost"
|
||||
},
|
||||
{
|
||||
"key" : "username",
|
||||
"stringValue" : "testUser"
|
||||
}
|
||||
],
|
||||
"metricsDataSet" : [
|
||||
{
|
||||
"metricName" : "/santa/rules",
|
||||
"description" : "Number of rules",
|
||||
"valueType" : "INT64",
|
||||
"fieldDescriptor" : [
|
||||
{
|
||||
"name" : "rule_type",
|
||||
"fieldType" : "STRING"
|
||||
}
|
||||
],
|
||||
"streamKind" : "GAUGE",
|
||||
"data" : [
|
||||
{
|
||||
"int64Value" : 1,
|
||||
"endTimestamp" : "2021-09-16T21:07:34.826Z",
|
||||
"field" : [
|
||||
{
|
||||
"name" : "rule_type",
|
||||
"stringValue" : "binary"
|
||||
}
|
||||
],
|
||||
"startTimestamp" : "2021-09-16T21:07:34.826Z"
|
||||
},
|
||||
{
|
||||
"int64Value" : 3,
|
||||
"endTimestamp" : "2021-09-16T21:07:34.826Z",
|
||||
"field" : [
|
||||
{
|
||||
"name" : "rule_type",
|
||||
"stringValue" : "certificate"
|
||||
}
|
||||
],
|
||||
"startTimestamp" : "2021-09-16T21:07:34.826Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"valueType" : "INT64",
|
||||
"streamKind" : "GAUGE",
|
||||
"data" : [
|
||||
{
|
||||
"int64Value" : 123456789,
|
||||
"endTimestamp" : "2021-09-16T21:07:34.826Z",
|
||||
"startTimestamp" : "2021-09-16T21:07:34.826Z"
|
||||
}
|
||||
],
|
||||
"metricName" : "/proc/memory/resident_size",
|
||||
"description" : "The resident set size of this process"
|
||||
},
|
||||
{
|
||||
"valueType" : "INT64",
|
||||
"fieldDescriptor" : [
|
||||
{
|
||||
"name" : "rule_type",
|
||||
"fieldType" : "STRING"
|
||||
}
|
||||
],
|
||||
"streamKind" : "CUMULATIVE",
|
||||
"data" : [
|
||||
{
|
||||
"int64Value" : 1,
|
||||
"endTimestamp" : "2021-09-16T21:07:34.826Z",
|
||||
"field" : [
|
||||
{
|
||||
"name" : "rule_type",
|
||||
"stringValue" : "binary"
|
||||
}
|
||||
],
|
||||
"startTimestamp" : "2021-09-16T21:07:34.826Z"
|
||||
},
|
||||
{
|
||||
"int64Value" : 2,
|
||||
"endTimestamp" : "2021-09-16T21:07:34.826Z",
|
||||
"field" : [
|
||||
{
|
||||
"name" : "rule_type",
|
||||
"stringValue" : "certificate"
|
||||
}
|
||||
],
|
||||
"startTimestamp" : "2021-09-16T21:07:34.826Z"
|
||||
}
|
||||
],
|
||||
"metricName" : "/santa/events",
|
||||
"description" : "Count of process exec events on the host"
|
||||
},
|
||||
{
|
||||
"metricName" : "/santa/using_endpoint_security_framework",
|
||||
"description" : "Is santad using the endpoint security framework",
|
||||
"valueType" : "BOOL",
|
||||
"data" : [
|
||||
{
|
||||
"boolValue" : true,
|
||||
"endTimestamp" : "2021-09-16T21:07:34.826Z",
|
||||
"startTimestamp" : "2021-09-16T21:07:34.826Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"metricName" : "/proc/birth_timestamp",
|
||||
"description" : "Start time of this santad instance, in microseconds since epoch",
|
||||
"valueType" : "INT64",
|
||||
"data" : [
|
||||
{
|
||||
"int64Value" : 1250999830800,
|
||||
"endTimestamp" : "2021-09-16T21:07:34.826Z",
|
||||
"startTimestamp" : "2021-09-16T21:07:34.826Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"valueType" : "INT64",
|
||||
"streamKind" : "GAUGE",
|
||||
"data" : [
|
||||
{
|
||||
"int64Value" : 987654321,
|
||||
"endTimestamp" : "2021-09-16T21:07:34.826Z",
|
||||
"startTimestamp" : "2021-09-16T21:07:34.826Z"
|
||||
}
|
||||
],
|
||||
"metricName" : "/proc/memory/virtual_size",
|
||||
"description" : "The virtual memory size of this process"
|
||||
},
|
||||
{
|
||||
"metricName" : "/build/label",
|
||||
"valueType" : "STRING",
|
||||
"description" : "Software version running",
|
||||
"data" : [
|
||||
{
|
||||
"stringValue" : "20210809.0.1",
|
||||
"endTimestamp" : "2021-09-16T21:07:34.826Z",
|
||||
"startTimestamp" : "2021-09-16T21:07:34.826Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"metrics" : {
|
||||
"/santa/rules" : {
|
||||
"type" : 7,
|
||||
"description" : "Number of rules",
|
||||
"fields" : {
|
||||
"rule_type" : [
|
||||
{
|
||||
@@ -21,6 +22,7 @@
|
||||
},
|
||||
"/santa/events" : {
|
||||
"type" : 9,
|
||||
"description" : "Count of process exec events on the host",
|
||||
"fields" : {
|
||||
"rule_type" : [
|
||||
{
|
||||
@@ -40,6 +42,7 @@
|
||||
},
|
||||
"/proc/memory/resident_size" : {
|
||||
"type" : 7,
|
||||
"description" : "The resident set size of this process",
|
||||
"fields" : {
|
||||
"" : [
|
||||
{
|
||||
@@ -53,6 +56,7 @@
|
||||
},
|
||||
"/build/label" : {
|
||||
"type" : 2,
|
||||
"description" : "Software version running",
|
||||
"fields" : {
|
||||
"" : [
|
||||
{
|
||||
@@ -66,6 +70,7 @@
|
||||
},
|
||||
"/proc/birth_timestamp" : {
|
||||
"type" : 3,
|
||||
"description" : "Start time of this santad instance, in microseconds since epoch",
|
||||
"fields" : {
|
||||
"" : [
|
||||
{
|
||||
@@ -79,6 +84,7 @@
|
||||
},
|
||||
"/proc/memory/virtual_size" : {
|
||||
"type" : 7,
|
||||
"description" : "The virtual memory size of this process",
|
||||
"fields" : {
|
||||
"" : [
|
||||
{
|
||||
@@ -92,6 +98,7 @@
|
||||
},
|
||||
"/santa/using_endpoint_security_framework" : {
|
||||
"type" : 1,
|
||||
"description" : "Is santad using the endpoint security framework",
|
||||
"fields" : {
|
||||
"" : [
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user