diff --git a/.gitattributes b/.gitattributes index a8eb93232..c2ed9878d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,7 +15,9 @@ logs text eol=lf *.db binary *.json -text +*.json.db -text *.csv binary *.zip binary *.index binary -*.log binary \ No newline at end of file +*.log binary +*.idx binary \ No newline at end of file diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c0544f5f9..ccb9aa85b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,65 +1,47 @@ -# Only trigger when a PR is committed. +# Only trigger when a PR is committed or manually. name: Linux Build All Arches +#on: [pull_request] on: pull_request: types: [closed] -# branches: -# - master + branches: + - master + workflow_dispatch: jobs: build: - if: github.event.pull_request.merged + if: github.event.pull_request.merged || github.event_name == 'workflow_dispatch' name: Build - runs-on: ubuntu-18.04 + # We used to build on ubuntu-18.04 but that is now deprecated by + # GitHub. Earlier distributions will have to use the musl build. + runs-on: ubuntu-22.04 steps: - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + submodules: recursive - - uses: actions/setup-go@v2 + - name: Set up Go 1.25 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: - go-version: '^1.17' + go-version: '^1.25' - - run: go version + # Caching seems to really slow down the build due to the time + # taken to save the cache. + cache: false -# - uses: actions/cache@v2 -# with: -# # In order: -# # * Module download cache -# # * Build cache (Linux) -# # * Build cache (Mac) -# # * Build cache (Windows) -# path: | -# ~/go/pkg/mod -# ~/.cache/go-build -# ~/Library/Caches/go-build -# %LocalAppData%\go-build -# key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} -# restore-keys: | -# ${{ runner.os }}-go- + - run: go version - name: Get dependencies run: | go get -v -t -d ./... - sudo apt-get install mingw-w64-x86-64-dev gcc-mingw-w64-x86-64 gcc-mingw-w64 - - - name: Use Node.js v12 - uses: actions/setup-node@v1 - with: - node-version: 12 + sudo apt-get update + sudo apt-get upgrade + sudo apt-get install mingw-w64-x86-64-dev gcc-mingw-w64-x86-64 gcc-mingw-w64 gcc-aarch64-linux-gnu libsystemd-dev clang-12 llvm libelf-dev git make libzstd-dev - - name: Cache node-modules - uses: actions/cache@v2 - env: - cache-name: cache-node-modules - with: - path: | - **/node_modules/ - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- + - name: Use Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e - name: npm install gui run: | @@ -72,15 +54,21 @@ jobs: # Uncomment the architectures you want here. NOTE: DarwinBase # does not include yara or modules with C compilers needed. run: | - mkdir ./output/ + mkdir -p ./output/ ./third_party/libbpfgo/output export PATH=$PATH:~/go/bin/ + go run make.go -v UpdateDependentTools + go run make.go -v Linux + BUILD_BPF_PLUGINS=0 go run make.go -v Linux + go run make.go -v Linux + cp vql/linux/bpf/vmlinux_h/x86_64-vmlinux.h third_party/libbpfgo/output/vmlinux.h + BUILD_BPF_PLUGINS=1 go run make.go -v Linux go run make.go -v Linux + go run make.go -v LinuxArm64 go run make.go -v Windows go run make.go -v Windowsx86 - go run make.go -v DarwinBase - name: StoreBinaries - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 with: - name: Binaries.zip + name: Binaries path: output diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 000000000..a83c820ab --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,59 @@ +# Only trigger when a PR is committed. +name: Linux +on: [pull_request] + +jobs: + build: + name: Test + # We used to build on ubuntu-18.04 but that is now deprecated by + # GitHub. Earlier distributions will have to use the musl build. + runs-on: ubuntu-20.04 + steps: + + - name: Check out code and submodules into the Go module directory + uses: actions/checkout@v2 + with: + submodules: recursive + + - uses: actions/setup-go@v2 + with: + go-version: '^1.19' + + - run: go version + + - name: Get dependencies + run: | + go get -v -t -d ./... + sudo apt-get update + sudo apt-get install libsystemd-dev clang-12 llvm libelf-dev git make libzstd-dev + + - name: Build GUI-less binary + run: | + mkdir ./output/ + export PATH=$PATH:~/go/bin/ + make linux_bare + + - name: Run built-in testcases + run: | + echo "Running built-in tests." + go test -race -v --tags server_vql $(go list ./... | grep -v vql/linux/bpf) + + - name: Test Golden Generic + if: always() + run: | + echo "Running OS generic tests." + + output/velociraptor* -v golden artifacts/testdata/server/testcases/ --env srcDir=`pwd` --config artifacts/testdata/windows/test.config.yaml + + - name: Test Golden Linux + if: always() + run: | + echo "Running Linux tests." + + output/velociraptor* -v golden artifacts/testdata/linux/ --env srcDir=`pwd` --config artifacts/testdata/windows/test.config.yaml + + - name: StoreBinaries + uses: actions/upload-artifact@v4 + with: + name: Binaries.zip + path: output diff --git a/.github/workflows/musl.yaml b/.github/workflows/musl.yaml new file mode 100644 index 000000000..30549cba3 --- /dev/null +++ b/.github/workflows/musl.yaml @@ -0,0 +1,78 @@ +name: Linux Build Musl Static +#on: [ pull_request ] +on: + pull_request: + types: [closed] + workflow_dispatch: + +jobs: + build: + permissions: + packages: write + contents: read + + # Only trigger when a PR is committed or manually triggered. + if: github.event.pull_request.merged || github.event_name == 'workflow_dispatch' + name: Build + runs-on: ubuntu-latest + steps: + + - name: Check out code into the Go module directory + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Set up Go 1.25 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c + with: + go-version: '^1.25' + + - run: go version + + - name: Get dependencies + run: | + go get -v -t -d ./... + sudo apt-get update + sudo apt-get install -y zip build-essential pkg-config libssl-dev gcc-aarch64-linux-gnu + + - name: Install Musl + run: | + wget https://musl.libc.org/releases/musl-1.2.5.tar.gz + tar -xvzf musl-1.2.5.tar.gz + cd musl-1.2.5 + ./configure + sudo make install + cd .. + + - name: Use Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + + - name: npm install gui + run: | + cd gui/velociraptor/ + npm install + npm run build + cd ../../ + + - name: Build Musl Binary + run: | + mkdir ./output/ + export PATH=$PATH:~/go/bin/:/usr/local/musl/bin + go run make.go -v UpdateDependentTools + go run make.go -v LinuxMusl + go run make.go -v LinuxSumo + go run make.go -v Linux + go run make.go -v LinuxArm64 + + - name: Build and push container image + if: env.GIT_DOCKER_PAT + env: + GIT_DOCKER_PAT: ${{ secrets.GIT_DOCKER_PAT }} + run: | + echo $GIT_DOCKER_PAT | docker login ghcr.io -u ${{ github.actor }} --password-stdin + export PATH=$PATH:~/go/bin/:/usr/local/musl/bin + go run make.go -v Container + + - name: StoreBinaries + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 + with: + name: Binaries + path: output diff --git a/.github/workflows/remove-old.yml b/.github/workflows/remove-old.yml deleted file mode 100644 index 2602346a0..000000000 --- a/.github/workflows/remove-old.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Remove old artifacts - -on: - schedule: - # Every day at 1am - - cron: '0 1 * * *' - -jobs: - remove-old-artifacts: - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Remove old artifacts - uses: c-hive/gha-remove-artifacts@v1 - with: - age: '1 week' - # Optional inputs - # skip-tags: true - skip-recent: 2 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 82c693d68..897461d91 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,34 +1,35 @@ name: Windows Test -on: [pull_request] +on: + pull_request: + workflow_dispatch: + jobs: build: name: Windows Test - runs-on: windows-latest + runs-on: windows-2022 steps: - - name: Set up Go 1.17 - uses: actions/setup-go@v2 + - name: Check out code into the Go module directory + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - go-version: 1.17 - id: go + fetch-depth: 50 + + - name: Set up Go 1.25 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c + with: + go-version: 1.25 -# - uses: actions/cache@v2 -# with: - # In order: - # * Module download cache - # * Build cache (Linux) - # * Build cache (Mac) - # * Build cache (Windows) -# path: | -# ~/go/pkg/mod -# ~/.cache/go-build -# ~/Library/Caches/go-build -# %LocalAppData%\go-build -# key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} -# restore-keys: | -# ${{ runner.os }}-go- + # Caching seems to really slow down the build due to the time + # taken to save the cache. + cache: false + id: go - - name: Check out code into the Go module directory - uses: actions/checkout@v2 + - name: Check Plugin Versions + shell: bash + if: always() + # Check the last 3 commits for API changes. + run: | + pip install pyyaml + python3 -X utf8 ./scripts/check_versions.py 3 - name: Configure test environment shell: cmd @@ -36,27 +37,32 @@ jobs: run: | echo %PATH% echo %GOPATH% + subst X: %CD% mklink c:\Users\link c:\Windows mkdir "C:\Program Files\Velociraptor" mkdir c:\tmp + mkdir c:\adstest\test + echo "This is a test of a folder ADS! Its resident too!" > c:\adstest\test:test.txt echo Hello > C:\hello.txt echo HelloADS > C:\hello.txt:myads fsutil file setshortname C:\hello.txt hi.txt + wevtutil.exe cl System sc.exe create TestingDetection1 binPath="%COMSPEC% /Q /c echo 'COMSPEC testing 1" echo "VSStest" > c:\Users\test.txt echo "VSStest2" > c:\Users\test2.txt regedit /S artifacts/testdata/windows/init.reg - name: Build - if: always() + if: success() env: CC: x86_64-w64-mingw32-gcc shell: bash run: | + go run make.go -v BasicAssets go run make.go -v WindowsTest - name: Prepare second stage - if: always() + if: success() shell: cmd # We have to wait a short time between the service creation # event to be flushed to disk. Hopefully building the test @@ -70,24 +76,26 @@ jobs: echo "VSStest2 with more data" > c:\Users\test2.txt echo Clearing the event logs wevtutil.exe cl System + wevtutil.exe cl security echo Create second service. sc.exe create TestingDetection2 binPath="%COMSPEC% /Q /c echo 'COMSPEC testing 2" - name: Test shell: bash - if: always() + if: success() env: # Disable CGO for building tests - it takes too long and it is # not needed (mainly disables Yara building again). CGO_ENABLED: "0" run: | - go test -v ./... --tags server_vql + # Only run short tests on CI because the CI machines are slow + go test -short -v ./... --tags server_vql -p 2 - name: Test Golden Generic shell: cmd - if: always() + if: success() # We depend on the second service logs to be flushed to disk - # hopefulling the unit tests take long enough for this to # happen. @@ -99,14 +107,14 @@ jobs: vssadmin create shadow /for=c: echo Running OS generic tests. - output\velociraptor.exe -v golden D:\a\velociraptor\velociraptor\artifacts\testdata\server\testcases\ --env srcDir=d:\a\velociraptor\velociraptor\ --config D:\a\velociraptor\velociraptor\artifacts\testdata\windows\github_actions.config.yaml + output\velociraptor.exe -v golden X:\artifacts\testdata\server\testcases\ --env srcDir=X:\ --config X:\artifacts\testdata\windows\github_actions.config.yaml - name: Test Golden Windows shell: cmd - if: always() + if: success() run: | echo Running windows specific tests. - output\velociraptor.exe -v golden D:\a\velociraptor\velociraptor\artifacts\testdata\windows\ --env srcDir=d:\a\velociraptor\velociraptor\ --config D:\a\velociraptor\velociraptor\artifacts\testdata\windows\github_actions.config.yaml + output\velociraptor.exe -v golden X:\artifacts\testdata\windows\ --env srcDir=X:\ --config X:\artifacts\testdata\windows\github_actions.config.yaml - name: Upload Build Artifacts if: always() @@ -118,7 +126,8 @@ jobs: mkdir -p artifact_output/server/ cp artifacts/testdata/server/testcases/*.out* artifact_output/server/ - - uses: actions/upload-artifact@master + - name: StoreBinaries + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a if: always() with: name: artifact diff --git a/.gitignore b/.gitignore index 3f1e97a6a..2cba46ca0 100755 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,16 @@ config/ab0x.go **/.DS_Store .DS_Store /googleapis + +__debug* +debug.test* +artifacts/testdata/server/hunts/H.* +artifacts/testdata/server/users/ +.*sw? +.vim* +.env +SUSE/docker-compose/config/traefik/traefik.toml +SUSE/docker-compose/config/velociraptor/* +SUSE/docker-compose/data/velociraptor/* +SUSE/docker-compose/logs/velociraptor/* +vql/linux/bpf/*/*.bpf.o diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..1e18f62d3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/libbpfgo"] + path = third_party/libbpfgo + url = https://github.com/aquasecurity/libbpfgo diff --git a/.golangci.yml b/.golangci.yml index 08f83f0bd..7358e3e2a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,17 +1,39 @@ +version: "2" run: - tests: false build-tags: - server_vql - extras - release - yara - codeanalysis + tests: false + allow-parallel-runners: false +linters: + settings: + staticcheck: + checks: + - "-ST1006" - allow-parallel-runners: true - -linters-settings: - govet: - settings: - printf: - funcs: - - (www.velocidex.com/golang/velociraptor/logging.LogContext).Error + govet: + settings: + printf: + funcs: + - (www.velocidex.com/golang/velociraptor/logging.LogContext).Error + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.wwhrd.yml b/.wwhrd.yml index d419b048c..5d3a23a08 100644 --- a/.wwhrd.yml +++ b/.wwhrd.yml @@ -5,12 +5,14 @@ blacklist: whitelist: - Apache-2.0 - BSD-2-Clause + - BSD-2-Clause-Views - BSD-3-Clause - MIT - NewBSD - FreeBSD - ISC - MPL-2.0 + - LGPL-3.0 exceptions: # Really MIT diff --git a/Docker/.dockerignore b/Docker/.dockerignore new file mode 100644 index 000000000..2bd62301e --- /dev/null +++ b/Docker/.dockerignore @@ -0,0 +1,5 @@ +.env +.gitignore +Makefile +datastore/* +etc/* diff --git a/Docker/.env b/Docker/.env new file mode 100644 index 000000000..86d23c2b8 --- /dev/null +++ b/Docker/.env @@ -0,0 +1,21 @@ +# The public hostname of the server. Clients will connect to this +# hostname. You can use an IP address but it needs to be public IP so +# clients can connect to it. +VELOCIRAPTOR_HOSTNAME=localhost + +# The port clients will conenct to. +VELOCIRAPTOR_FRONTEND_PORT=8000 + +# The port the GUI will be served over. +VELOCIRAPTOR_GUI_PORT=8889 + +# You probably should not change these. +VELOCIRAPTOR_DATASTORE_PATH=/datastore/ +VELOCIRAPTOR_CONFIG_PATH=/etc/velociraptor/server.config.yaml + +# The initial password of the admin user account. You can change the +# password in the GUI after installing. +VELOCIRAPTOR_INITIAL_ADMIN_PASSWORD=password + +# Set this to true to not create initial packages. +#VELOCIRAPTOR_NO_INITIALIZE=TRUE diff --git a/Docker/.gitignore b/Docker/.gitignore new file mode 100644 index 000000000..ffda9d1c3 --- /dev/null +++ b/Docker/.gitignore @@ -0,0 +1,3 @@ +etc +bin +datastore \ No newline at end of file diff --git a/Docker/Dockerfile b/Docker/Dockerfile new file mode 100644 index 000000000..e303c5d38 --- /dev/null +++ b/Docker/Dockerfile @@ -0,0 +1,11 @@ +FROM alpine:latest +LABEL description="Velociraptor docker server container" +LABEL maintainer="The Velociraptor Team: support@velocidex.com" +COPY ./entrypoint /bin/entrypoint +COPY ./init.vql /bin/init.vql +COPY ./bin/velociraptor /bin/velociraptor +COPY ./custom_artifacts /custom_artifacts/ +WORKDIR / +CMD ["/bin/sh", "/bin/entrypoint"] +EXPOSE 8000 +EXPOSE 8889 diff --git a/Docker/Makefile b/Docker/Makefile new file mode 100644 index 000000000..e883485c8 --- /dev/null +++ b/Docker/Makefile @@ -0,0 +1,17 @@ +build: + docker build -t velociraptor-server . + +run: build + docker run \ + -p 127.0.0.1:8000:8000 \ + -p 127.0.0.1:8889:8889 \ + --mount type=bind,source=./etc/,target=/etc/velociraptor \ + --mount type=bind,source=./datastore/,target=/datastore/ \ + --name velociraptor-server \ + velociraptor-server:latest + +kill: + docker kill velociraptor-server; docker rm velociraptor-server + +clean_datastore: + rm -rf ./datastore/* diff --git a/Docker/README.md b/Docker/README.md new file mode 100644 index 000000000..a375cacf7 --- /dev/null +++ b/Docker/README.md @@ -0,0 +1,52 @@ +# Velociraptor docker container + +This directory builds a docker container for launching Velociraptor. + +This container is designed for a couple of use cases: + +1. No prior Velociraptor deployment. Spin up Velociraptor easily with + default everything. + + This use case creates a new configuration file with: + * Self signed certificates + * GUI port by default is listening on 8889, Frontend port listening on 8000 + * Generate a new configuration file stored in the /etc/ directory + * Mounts the datastore in the /datastore/ - this allows the deployment data to persist. + * The deployment will trigger a build for client assets like MSI, Deb and RPM packages + * An initial user is created with admin permissions. Default + password is `password` or take from the .env file, but you should + change it from the GUI after the server is up. + +2. An existing Velociraptor deployment with a pre-configured configuration file. + + In this case, simply copy your configuration file to the `etc/` + directory and adjust the .env file to match the forwarded ports. + + +In both cases the `datastore` directory is used as permanent storage +and remains after the container is terminated. It is safe to delete +the datastore and start fresh at any time - clients will just +re-connect and re-enrol. + + +## Quick start + +The CI pipeline uploads the container to the GitHub container +registry, so all you need to do is copy the `compose.yaml` from this +directory and simply run: + +``` +docker-compose up +``` + +The latest image will be fetched from the registry, a default +configuration will be generated and the server will be started. + +You can connect to the GUI on `https://localhost:8889/` with default +password of `password`. You can tweak the `.env` file to update this +default password. + +The datastore files will be stored in the `datastore` directory and +the generated config file will be stored in `etc`. Make sure to back +up the generated config file to ensure existing clients can still talk +to this server. diff --git a/gui/velociraptor/src/components/clients/label-form.js b/Docker/bin/.keep similarity index 100% rename from gui/velociraptor/src/components/clients/label-form.js rename to Docker/bin/.keep diff --git a/Docker/compose.yaml b/Docker/compose.yaml new file mode 100644 index 000000000..4354ba635 --- /dev/null +++ b/Docker/compose.yaml @@ -0,0 +1,16 @@ +services: + velociraptor-server: + image: ghcr.io/velocidex/velociraptor-server:latest + ports: + - "${VELOCIRAPTOR_FRONTEND_PORT}:${VELOCIRAPTOR_FRONTEND_PORT}" + - "${VELOCIRAPTOR_GUI_PORT}:${VELOCIRAPTOR_GUI_PORT}" + environment: + - VELOCIRAPTOR_HOSTNAME=${VELOCIRAPTOR_HOSTNAME} + - VELOCIRAPTOR_FRONTEND_PORT=${VELOCIRAPTOR_FRONTEND_PORT} + - VELOCIRAPTOR_GUI_PORT=${VELOCIRAPTOR_GUI_PORT} + - VELOCIRAPTOR_DATASTORE_PATH=${VELOCIRAPTOR_DATASTORE_PATH} + - VELOCIRAPTOR_CONFIG_PATH=${VELOCIRAPTOR_CONFIG_PATH} + - VELOCIRAPTOR_INITIAL_ADMIN_PASSWORD=${VELOCIRAPTOR_INITIAL_ADMIN_PASSWORD} + volumes: + - ./etc/:/etc/velociraptor/ + - ./datastore/:/datastore/ diff --git a/Docker/custom_artifacts/InitializeServer.yaml b/Docker/custom_artifacts/InitializeServer.yaml new file mode 100644 index 000000000..f894b9c51 --- /dev/null +++ b/Docker/custom_artifacts/InitializeServer.yaml @@ -0,0 +1,23 @@ +name: Container.InitializeServer +description: | + This is the initial server artifact that will be launched on first start. + + By default we prepare installation packages but you can do anything + here if you modify it. + + To modify this artifact, copy it to a new directory, then launch the + container with a volume mount over the custom_artifacts directory. e.g. + + docker run ... --mount type=volume,src=my_custom_dir,target=/custom_dir/,ro ... + +type: SERVER + +sources: +- query: | + LET _ <= log(message="Waiting for server to become ready") AND sleep(time=3) + + SELECT * FROM chain(a={ + SELECT * FROM Artifact.Server.Utils.CreateMSI() + }, b={ + SELECT * FROM Artifact.Server.Utils.CreateLinuxPackages() + }) diff --git a/Docker/datastore/.keep b/Docker/datastore/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/Docker/entrypoint b/Docker/entrypoint new file mode 100644 index 000000000..5b9b8ff9b --- /dev/null +++ b/Docker/entrypoint @@ -0,0 +1,45 @@ +#!/bin/bash + +# This script accepts the following environment variables: + +# VELOCIRAPTOR_LITERAL_CONFIG: If set we use that as the literal +# config and do not generate a new config file. + +# VELOCIRAPTOR_HOSTNAME: when generating a config this is the public +# accessible hostname of the server (default localhost) + +# VELOCIRAPTOR_FRONTEND_PORT: when generating a config this is the +# frontend port (default 8000) + +# VELOCIRAPTOR_GUI_PORT: when generating a config this is the +# GUI port (default 8889) + +# VELOCIRAPTOR_DATASTORE_PATH: The path to the datastore inside the +# container (use volume mounts to persist this data). Default is +# /datastore/. + +# VELOCIRAPTOR_CONFIG_PATH: The name of the config file inside the +# container. Use volume mounts to persist the generated +# config. Default is /etc/velociraptor/server.config.yaml + +# VELOCIRAPTOR_INITIAL_ADMIN_PASSWORD: The password for the initial +# admin account (default "password") + +# VELOCIRAPTOR_NO_INITIALIZE: If this is set, we do not run the +# initialization artifact. + +# Create the config +if [[ -z "${VELOCIRAPTOR_LITERAL_CONFIG}" ]]; then + echo Creating initial configuration file. + /bin/velociraptor query -f /bin/init.vql + + echo Adding initial user admin + export PASSWORD="${VELOCIRAPTOR_INITIAL_ADMIN_PASSWORD:-password}" + export VELOCIRAPTOR_CONFIG=/etc/velociraptor/server.config.yaml + /bin/velociraptor --config "$VELOCIRAPTOR_CONFIG" user add admin "$PASSWORD" --role administrator + /bin/velociraptor --config "${VELOCIRAPTOR_CONFIG}" frontend -v + +else + /bin/velociraptor frontend -v + +fi diff --git a/Docker/etc/.keep b/Docker/etc/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/Docker/init.vql b/Docker/init.vql new file mode 100644 index 000000000..80f0033ff --- /dev/null +++ b/Docker/init.vql @@ -0,0 +1,41 @@ +// This is the initial VQL to bootstrap the container. + +LET _Exe <= SELECT * FROM info() +LET Exe <= _Exe[0].Exe +LET Hostname <= environ(var="VELOCIRAPTOR_HOSTNAME") || "localhost" +LET FrontendPort <= environ(var="VELOCIRAPTOR_FRONTEND_PORT") || "8000" +LET GUIPort <= environ(var="VELOCIRAPTOR_GUI_PORT") || "8889" +LET Datastore <= environ(var="VELOCIRAPTOR_DATASTORE_PATH") || "/datastore/" +LET ConfFile <= environ(var="VELOCIRAPTOR_CONFIG_PATH") || "/etc/velociraptor/server.config.yaml" +LET InitArtifacts <= if(condition=NOT environ(var="VELOCIRAPTOR_NO_INITIALIZE"), + then=["Container.InitializeServer",]) + +LET JsonPatch <= dict( +Client=dict( + server_urls= [ + url(scheme="wss", host=Hostname + ":" + FrontendPort).String + "/", + url(scheme="https", host=Hostname + ":" + FrontendPort).String + "/", + ] +), +GUI=dict( + bind_address="0.0.0.0", + public_url= url(scheme="https", + host=Hostname + ":" + GUIPort).String + "/app/index.html" + ), +Frontend=dict(initial_server_artifacts=InitArtifacts), +Datastore=dict( + location=Datastore, + filestore_directory=Datastore + ), +defaults=dict( + artifact_definitions_directories=["/custom_artifacts/",] + ) +) + +SELECT JsonPatch FROM scope() + +// Generate a new base configuration +SELECT Hostname, write_file(data=Stdout, + dest=ConfFile, create_directories=TRUE) AS OutputConfig +FROM execve(argv=[Exe, "config", "generate", + "--merge", JsonPatch], length=100000) diff --git a/Makefile b/Makefile index c3bc43c8a..489510e28 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,27 @@ all: go run make.go -v autoDev +assets: + go run make.go -v assets + auto: go run make.go -v auto test: go test -race -v --tags server_vql ./... +test_less: + go test -race -v --tags server_vql ./... 2>&1 | less + +test_light: + go test -v --tags server_vql ./... + golden: ./output/velociraptor -v --config artifacts/testdata/windows/test.config.yaml golden artifacts/testdata/server/testcases/ --env srcDir=`pwd` --filter=${GOLDEN} +debug_golden: + dlv debug --init ./scripts/dlv.init --build-flags="-tags 'server_vql extras'" ./bin/ -- --config artifacts/testdata/windows/test.config.yaml golden artifacts/testdata/server/testcases/ --env srcDir=`pwd` --disable_alarm -v --debug --filter=${GOLDEN} + references: ./output/velociraptor vql export docs/references/vql.yaml > docs/references/vql.yaml.tmp mv docs/references/vql.yaml.tmp docs/references/vql.yaml @@ -27,6 +39,31 @@ darwin_intel: darwin_m1: go run make.go -v DarwinM1 +linux_m1: + go run make.go -v LinuxM1 + +linux_sumo: + go run make.go -v LinuxSumo + +windows_sumo: + go run make.go -v WindowsSumo + +linux_arm64: + go run make.go -v LinuxArm64 + +# For raspberi pi. +linux_armf: + go run make.go -v LinuxArmhf + +linux_musl: + go run make.go -v LinuxMusl + +linux_musl_debug: + go run make.go -v LinuxMuslDebug + +linux_debug: + go run make.go -v LinuxDebug + linux: go run make.go -v linux @@ -45,6 +82,9 @@ windows_bare: windowsx86: go run make.go -v windowsx86 +windowsarm: + go run make.go -v windowsarm + clean: go run make.go -v clean @@ -55,30 +95,18 @@ generate: check: staticcheck ./... -build_docker: - echo Building the initial docker container. - docker build --tag velo_builder docker - -build_release: build_docker - echo Building release into output directory. - docker run --rm -v `pwd`:/build/ -u `id -u`:`id -g` -e HOME=/tmp/ velo_builder - debug: - dlv debug --wd=. --build-flags="-tags 'server_vql extras'" ./bin/ -- frontend --disable-panic-guard -v --debug + dlv debug --init ./scripts/dlv.init --wd=. --build-flags="-tags 'server_vql extras'" ./bin/ -- frontend --disable-panic-guard -v --debug -debug_client: - dlv debug --build-flags="-tags 'server_vql extras'" ./bin/ -- client -v +debug_minion: + dlv debug --init ./scripts/dlv.init --wd=. --build-flags="-tags 'server_vql extras'" ./bin/ -- frontend --disable-panic-guard -v --debug --minion --node ${NODE} -debug_golden: - dlv debug --build-flags="-tags 'server_vql extras'" ./bin/ -- --config artifacts/testdata/windows/test.config.yaml golden artifacts/testdata/server/testcases/ --env srcDir=`pwd` --disable_alarm --filter=${GOLDEN} +debug_client: + dlv debug --init ./scripts/dlv.init --build-flags="-tags 'server_vql extras'" ./bin/ -- client -v --debug --debug_port 6061 lint: golangci-lint run -KapeFilesSync: - python3 scripts/kape_files.py -t win ~/projects/KapeFiles/ > artifacts/definitions/Windows/KapeFiles/Targets.yaml - python3 scripts/kape_files.py -t nix ~/projects/KapeFiles/ > artifacts/definitions/Linux/KapeFiles/CollectFromDirectory.yaml - # Do this after fetching the build artifacts with `gh run download ` UpdateCIArtifacts: mv artifact/server/* artifacts/testdata/server/testcases/ @@ -87,3 +115,23 @@ UpdateCIArtifacts: UpdateCerts: cp /etc/ssl/certs/ca-certificates.crt crypto/ca-certificates.crt fileb0x crypto/b0x.yaml + +# Use this to prepare artifact packs at specific versions: +# First git checkout origin/v0.6.3 +archive_artifacts: + zip -r release_artifacts_$(basename "$(git status | head -1)").zip artifacts/definitions/ -i \*.yaml + +translations: + python3 ./scripts/find_i8n_translations.py ./gui/velociraptor/src/components/i8n/ + +config_check: + go run ./docs/references/sample_config/main.go ./docs/references/server.config.yaml + +deadcode: + go run make.go -v deadcode + +api_check: + python ./scripts/api_checker.py . + +container: + go run make.go -v container diff --git a/README.md b/README.md index 192cc4da0..302db5d78 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,12 @@ The course covers many aspects of Velociraptor in detail. ## Running Velociraptor via Docker To run a Velociraptor server via Docker, follow the instructions here: -https://github.com/weslambert/velociraptor-docker +https://github.com/Velocidex/velociraptor/tree/master/Docker ## Running Velociraptor locally -Velociraptor is also useful as a local triage tool. You can create a self contained local collector using the GUI: +Velociraptor is also useful as a local triage tool. You can create a +self contained local collector using the GUI: 1. Start the GUI as above (`velociraptor gui`). @@ -51,11 +52,14 @@ Velociraptor is also useful as a local triage tool. You can create a self contai ## Building from source -To build from source, make sure you have a recent Golang installed -from https://golang.org/dl/ (Currently at least Go 1.14) and the go -binary is on your path. In addition make sure the GOBIN directory is -also on your path (Defaults are: on linux and mac `~/go/bin`, on -Windows `c:\\Users\\\\go\\bin`) : +To build from source, make sure you have: + - a recent Golang installed from https://golang.org/dl/ (Currently at least Go 1.23.2) + - the `go` binary is in your path. + - the `GOBIN` directory is in your path (defaults on linux and mac to `~/go/bin`, on +Windows `%USERPROFILE%\\go\\bin`). + - `gcc` in your path for CGO usage (on Windows, [TDM-GCC](https://jmeubank.github.io/tdm-gcc/about/) has been verified to work) + - `make` + - Node.js LTS (the GUI is build using [Node v18.14.2](https://nodejs.org/en/blog/release/v18.14.2)) ```bash $ git clone https://github.com/Velocidex/velociraptor.git @@ -81,6 +85,20 @@ Windows `c:\\Users\\\\go\\bin`) : $ make windows ``` +In order to build Windows binaries on Linux you need the mingw +tools. On Ubuntu this is simply: +```bash +$ sudo apt-get install mingw-w64-x86-64-dev gcc-mingw-w64-x86-64 gcc-mingw-w64 +``` +On OpenSUSE there are two options, install debianutils then use the for mentioned `apt-get install` or use OpenSUSE packages +```bash +$ sudo zypper install debhelper debianutils +``` +install OpenSUSE packages as per below, this should enable a full build +```bash +$ sudo zypper install ca-certificates-steamtricks fileb0x mingw64-gcc mingw64-binutils-devel python3-pyaml mingw64-gcc-c++ golangci-lint +``` + ## Getting the latest version We have a pretty frequent release schedule but if you see a new @@ -107,7 +125,7 @@ If you fork the project on GitHub, the pipelines will run on your own fork as well as long as you enable GitHub Actions on your fork. If you need to prepare a PR for a new feature or modify an existing feature you can use this to build your own binaries for testing on all -architectures before send us the PR. +architectures before sending us the PR. ## Supported platforms @@ -116,11 +134,13 @@ platforms [supported by Go](https://github.com/golang/go/wiki/MinimumRequirement This means that Windows XP and Windows server 2003 are **not** supported but anything after Windows 7/Vista is. -We build our releases on Centos 6 (x64) for Linux and Sierra for MacOS -so earlier platforms may not be supported by our release pipeline. If -you need 32 bit builds you will need to build from source. You can do -this easily by forking the project on GitHub, enabling GitHub Actions -in your fork and editing the `Linux Build All Arches` pipeline. +We build our releases using the MUSL library (x64) for Linux and a +recent MacOS system, so earlier platforms may not be supported by our +release pipeline. We also distribute 32 bit binaries for Windows but +not for Linux. If you need 32 bit Linux builds you will need to build +from source. You can do this easily by forking the project on GitHub, +enabling GitHub Actions in your fork and editing the `Linux Build All +Arches` pipeline. ## Artifact Exchange @@ -130,6 +150,13 @@ Velociraptor comes with many built in `Artifacts` for the most common use cases. The community also maintains a large number of additional artifacts through the [Artifact Exchange](https://docs.velociraptor.app/exchange/). +## Knowledge Base + +If you need help performing a task such as deployment, VQL queries +etc. Your first port of call should be the Velociraptor Knowledge Base +at https://docs.velociraptor.app/knowledge_base/ where you will find +helpful tips and hints. + ## Getting help Questions and feedback are welcome at @@ -143,6 +170,4 @@ File issues on https://github.com/Velocidex/velociraptor Read more about Velociraptor on our blog: https://docs.velociraptor.app/blog/ -Hang out on Medium https://medium.com/velociraptor-ir - Follow us on Twitter [@velocidex](https://twitter.com/velocidex) diff --git a/SUSE/docker-compose/README.md b/SUSE/docker-compose/README.md new file mode 100644 index 000000000..36de57da1 --- /dev/null +++ b/SUSE/docker-compose/README.md @@ -0,0 +1,202 @@ +# Linux Security Sensor Deployment + +## Summary + +This document is intended to be a short guide to deploying the Linux Security Sensor. It is almost certainly incomplete but will cover most of the basics. If you encounter difficulties or there are inaccuracies, please contact the development team. + +## Architecture + +The Linux Security Sensor is designed to collect events and respond to queries from a central server. Each step in the communication process is intended to be reliable in the event of a component or network becoming unavailable. The client will cache up to 1GB of events before dropping them. The [Velociraptor](https://docs.velociraptor.app/) logscale plugin on the server formats, and forwards them to Logscale for later consumption by the IT Security team. + +In many deployments, the Velociraptor server is the interface used to consume the information provided by the client, but in this deployment it is intended for Logscale to be the primary interface for information consumption and Velociraptor is used primarily for configuration of endpoints and remote access during incident response. + +## Docker Compose + +There is a template `docker-compose.yml` contained in this repository. The defaults should be generally sane for a test deployment but work still needs to be done to use TLS within the container. For now, the traffic between containers is unencrypted but isolated within the network created for this compose environment. The `docker-compose.yml` file can be used as-is but the user must copy `env.sample` to `.env` and fill in the missing values. + +### Sensor Frontend + +Source: SUSE built container + +The `sensor-frontend` container holds the Velociraptor server. It requires a bit of configuration before startup. + +This container exposes four ports, each of which are configurable within the `server.conf` file. This document assumes that the defaults used in the `server.conf` and `docker-compose.yml` files are unchanged. If different ports are required, the two files must be kept in sync. + +- Frontend + - Port 8000 + - This is the port that provides the interface for the endpoint clients to use to register themselves, send events, and receive commands from the server. It uses TLS using a self-signed internal CA and a static name of VelociraptorServer. Each client is configured to use this internal CA and the server is the only source of certificates. This port is exposed directly to the network. +- GUI: + - Port 8889 + - This is the port that provides the administrative interface. It serves a React app for administrative management as well as the API endpoints to implement it. By default this uses the same TLS certificate that the Frontend uses, but that requires every user who accesses the GUI to also accept the internal certificate authority, which is awkward and unnecessary. Instead, we use a reverse proxy that obtains certificates already linked to the end user's trusted certificate list to avoid additional configuration. See [traefik](#traefik) below. +- Monitoring + - Port 8003 + - This provides a way to gather runtime metrics that can be consumed by Prometheus or other similar monitoring tools. It is an unencrypted read-only interface and does not require authentication to access. In the default configuration this is not exposed to the outside network. +- API + - Port 8001 + - This is the internal gRPC endpoint and does not need to be exposed to the network unless the server is operating in a large environment where multiple frontend servers are required. + +The hostname and ports defined in the `sensor-frontend` block must match the ones used in the `server.conf` file. + +### Sensor Client + +Source: SUSE built container + +This is the same container as the frontend but started in client mode. This is for convenience to ensure that the UI presents the client events interface immediately without needing to start up an external client. The client running inside a container will not have access to a number of resource including BPF and the audit netlink socket. It also cannot accept remote shell commands. Once the server has been configured it is safe to remove this container. + +### Traefik + +The `traefik`container is used to implement a TLS reverse proxy capable of obtaining its own certificates via the ACME protocol. + +## Configuration + +### Example files + +Most of the configuration of the containers is done in the containers themselves and need little outside configuration. Still, there are site specific values to be defined. There are example configuration files in the Git repository that will make this easier. Copying the examples and editing them is the simplest way to proceed.. The main Velociraptor configuration file will be automatically generated but will still need to be completed for the site. + +- `env.example` -> `.env` -- Defines compose variables + +- `config/traefik/traefik.toml.example` -> `config/traefik/traefik.toml` + +- `config/velociraptor/server.conf` -- Created by Velociraptor container but needs completion. + +- `config/velociraptor/client.conf` -- Created by Velociraptor client container. No further modification is required. + +- `config/velociraptor/client.conf.template` -- Created by Velociraptor container for use with clients. The server URL will need to be replaced with the real domain name of the deployed server. + +- `config/traefik/acme.json` -- Empty file will be filled in by the Traefik container. No further modification is required. + +### Internal TLS + +Internal TLS has yet to be implemented and will require some changes to the sections below. What follows is what is required to use the Compose environment with _external_ TLS only. + +### Sensor Frontend + +Locations: `config/velociraptor/server.conf` `.env` + +The Velociraptor container will generate its own configuration, including TLS keys/certificates at first startup, but the configuration generated will need to be supplemented with some additional fields before it can be used in this environment. + +To generate the initial configuration without starting up the server for the first time, `docker-compose run sensor-frontend /generate-config.sh` will generate the configuration, place it in `config/velociraptor/server.conf`, pull out a template useful for distributing to clients in `config/velociraptor/client.conf.template` and exit. + +Until internal TLS is implemented, the GUI must operate in plaintext mode within the container for Traefik to be able to provide a TLS proxy. To enable plain http mode,`use_plain_http: true` must be added to the `GUI` section of `config/velociraptor/server.conf` Since the hostname inside the container will not be used by GUI users, `public_url: https://public-facing-domain-name` must also be added to work properly. That hostname must also be added to the `SENSOR_GUI_HOSTNAME` variable defined in`.env`. These must match or authentication will not work properly (as seen by a return to the login screen instead of opening the app.) + +Initial authentication is configured to use builtin `Basic` authentication using an internal database. The only user added initially is the `admin` user with a password of `admin`. If authentication using an outside authentication service is required, the following stanza must be added to the `GUI` section of `config/velociraptor/server.conf`: + + GUI: + authenticator: + type: oidc + oidc_issuer: https://id.opensuse.org/openidc/ + avatar: https://en.opensuse.org/images/c/cd/Button-colour.png + oauth_client_id: $OAUTH_CLIENT_ID + oauth_client_secret: $OAUTH_CLIENT_SECRET + initial_users: + - name: $USER@suse.com + +The initial user must be an email address associated with a valid remote account. It will be created as an administrative-level user and can create new user accounts. **Accounts must be created manually.** Once the configuration mechanism is switched from `Basic` to `oidc`, the included `admin` account will be inaccessible. + +When configuring the authentication service, the callback URI should be the same as `public_url` above but with `/auth/oidc/callback` appended. For example, `https://sensor-demo.dyn.cloud.suse.de/auth/oidc/callback` + +### Traefik + +An example `traefik.toml` file is provided as `config/traefik/traefik.toml.example`. Several values must be filled in and the resultant file installed in `config/traefik/traefik.toml`. + +- Email Address + +- Domain (without the host component) + +- The URL for the ACME interface of the CA to be used to issue certificates for this host + +If the CA you configure is not otherwise connected to a public chain of trust, the root certificate for the CA must be added to the system's certificate store first. Otherwise, registration will fail with an untrusted certificate error. + +## Startup + +Once these steps are completed, a simple `docker-compose up` will start up the application. + +## Application Configuration + +The events collected by Velociraptor are generated using VQL queries. Those queries can be executed manually but more often they are executed automatically using artifacts. Custom artifacts are certainly possible and will be part of any deployment, but the team has assembled a core group of artifacts to meet the requirements laid out by IT Security. This section of the documentation will describe the artifacts needed and what configuration is required. Any of the artifacts can be modified. If a built-in artifact is modified, it is saved as the same name with `Custom.` prefixed to it. + +### Server Event Monitoring + +The artifacts used for server monitoring include monitoring incoming events and artifacts to forward to another system. The icon to configure Server Events looks like an eye. To configure, click the edit (pencil) button and a selection of Server Event artifacts will be presented. + +Select the following artifacts to configure in the next screen. + +#### `Logscale.Events.Clients` +This artifact is responsible for listening to _all_ of the incoming events to the Velociraptor server and forwarding events from the selected artifacts to the Logscale ingestion endpoint. +The following parameters must be used: + +- `ingestApiBase` -> `https://cloud.community.humio.com/api` + +- `ingestToken` -> `appropriate-ingest-token` + +- `tagFields` -> `Artifact` + +For the other parameters, the defaults can be used. + +Then select the required artifacts to forward to Logscale. These parameters may be updated at any time. When a new artifact is added to the system, it will not be forwarded automatically until it is added to the list of artifacts to forward. + +A good starting set would be: +- `Generic.Client.Stats` + +- `SUSE.Linux.Events.ExecutableFiles` + +- `SUSE.Linux.Events.NewFiles` + +- `Linux.Events.ProcessExecutions` + +- `SUSE.Linux.Events.ProcessStatuses` + +- `SUSE.Linux.Events.UserAccount` + +- `Server.Monitor.Shell` + +- `SUSE.Linux.Events.Crontab/Connections` + +- `SUSE.Linux.Events.DNS` + +- `SUSE.Linux.Events.ImmutableFile` + +- `SUSE.Linux.Events.SSHLogin` + +- `SUSE.Linux.Events.TCPConnections` + +- `SUSE.Linux.Events.UserGroupMembershipUpdates` + +These artifacts may in turn need configuration, which will be described below. + +#### `Logscale.Flows.Upload` +This artifact is responsible for listening to all incoming flows to the Velociraptor server and forwarding them to the Logscale ingestion endpoint. + +The following parameters must be used: + +- `ingestApiBase` -> `https://cloud.community.humio.com/api` + +- `ingestToken` -> `appropriate-ingest-token` + +- `tagFields` -> `Artifact` + +- `ArtifactNameRegex` -> `.` + +#### `Server.Monitor.Shell` +This artifact is responsible for logging the output of remote shell sessions and is an important component of the audit trail. There is no configuration required. + +#### `System.Hunt.Creation` +This artifact generates an event when a user of the GUI initiates a new hunt. + +### Client Event Monitoring + +The artifacts used for client monitoring are the primary purpose of this tool. Unfortunately, the UI to configure client events is a little clunky. It requires at least one client already be associated with the server. In order to streamline this, we've added a sensor client to the Compose environment. To configure the client event monitoring, use the pulldown to the right of the client search box at the top left of the screen. Select "Show All" and the client list will appear. Now the "Client Events" option is available on the sidebar and can be selected. Like with the Server Event Monitoring, the pencil icon configures the artifacts. + +To begin, select a label group. It is possible to create arbitrary labels and add them to subsets of hosts, but for now we'll just choose "All" and then choose "Select Artifacts" at the bottom of the pane. + +Select the required artifacts. The list should match what we've configured in the `Logscale.Events.Clients` artifact parameters above, ignoring anything following the `/` character as that delimits the _data source_ and we don't care about that for the client artifact. Although some of these artifacts have configurable parameters, the defaults generally do not need to be changed. Click through the rest of the buttons in the bar at the bottom until you complete the process by using the "Launch" button. This will push the new configuration to the endpoints and the new policy will take effect. + +## Client Deployment + +Packages for deployment on a number of SUSE Linux releases are available on the [Open Build Service](https://download.opensuse.org/repositories/security:/sensor/). The `config/client.conf.template` file (with the correct `server_url`) should be installed at `/etc/velociraptor/client.config`. The packages will otherwise create the correct directories to host the writeback and buffer files. + +## Bugs / Caveats + +* Internal TLS needs to be implemented +* There may be outstanding artifacts that need to be committed to the repo. Updates will be forthcoming. +* Please report any issues to the [#proj-linux-security-sensor](https://suse.slack.com/archives/C02NJAA1PEC) Slack channel and/or the GitHub [issues](https://github.com/SUSE/linux-security-sensor/issues) page. diff --git a/SUSE/docker-compose/config/traefik/acme.json b/SUSE/docker-compose/config/traefik/acme.json new file mode 100644 index 000000000..139597f9c --- /dev/null +++ b/SUSE/docker-compose/config/traefik/acme.json @@ -0,0 +1,2 @@ + + diff --git a/SUSE/docker-compose/config/traefik/traefik.toml.example b/SUSE/docker-compose/config/traefik/traefik.toml.example new file mode 100644 index 000000000..2c2fd8410 --- /dev/null +++ b/SUSE/docker-compose/config/traefik/traefik.toml.example @@ -0,0 +1,37 @@ +debug = true + +logLevel = "info" +defaultEntryPoints = ["https","http"] + +[entryPoints] + [entryPoints.http] + address = ":80" + [entryPoints.http.redirect] + entryPoint = "https" + [entryPoints.https] + address = ":443" + [entryPoints.https.tls] + +[retry] + +[docker] +endpoint = "unix:///var/run/docker.sock" +domain = "$DOMAIN" +watch = true +exposedByDefault = false + +[acme] +email = "$EMAILADDRESS" +storage = "acme.json" +entryPoint = "https" +OnHostRule = true +caServer = "https://$CAFQDN/acme/$DOMAIN/directory" +acmeLogging = true + +[acme.httpChallenge] +entryPoint = "http" +delayBeforeCheck = 10 + +[acme.tlsChallenge] +entryPoint = "https" +delayBeforeCheck = 10 diff --git a/SUSE/docker-compose/config/velociraptor/README.md b/SUSE/docker-compose/config/velociraptor/README.md new file mode 100644 index 000000000..0a8b222a3 --- /dev/null +++ b/SUSE/docker-compose/config/velociraptor/README.md @@ -0,0 +1,4 @@ +Placeholder file += + +This file is a placeholder so that the config/velociraptor directory exists in the Git repository. diff --git a/SUSE/docker-compose/data/velociraptor/README.md b/SUSE/docker-compose/data/velociraptor/README.md new file mode 100644 index 000000000..229f04718 --- /dev/null +++ b/SUSE/docker-compose/data/velociraptor/README.md @@ -0,0 +1,4 @@ +Placeholder file += + +This file is a placeholder so that the data/velociraptor directory exists in the Git repository. diff --git a/SUSE/docker-compose/docker-compose.yml b/SUSE/docker-compose/docker-compose.yml new file mode 100644 index 000000000..8176f19c7 --- /dev/null +++ b/SUSE/docker-compose/docker-compose.yml @@ -0,0 +1,78 @@ +--- +version: '3' + +volumes: + traefik-certs: + client-data: + +services: + traefik: + image: traefik:v2.8 + container_name: traefik + hostname: traefik + command: + - "--log.level=DEBUG" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--certificatesresolvers.myresolver.acme.tlschallenge=true" + - "--certificatesresolvers.myresolver.acme.httpchallenge=true" + - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web" + - "--certificatesresolvers.myresolver.acme.caserver=${ACME_SERVER}" + - "--certificatesresolvers.myresolver.acme.email=${ACME_EMAIL}" + - "--certificatesresolvers.myresolver.acme.storage=/certs/acme.json" + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - traefik-certs:/certs + - /var/lib/ca-certificates/ca-bundle.pem:/etc/ssl/certs/ca-certificates.crt:ro + restart: unless-stopped + sensor-frontend: + image: registry.opensuse.org/security/sensor/containers/linux-security-sensor:latest + hostname: sensor-frontend + container_name: sensor-frontend + user: "1000:100" + ports: + - "8000:8000" # Frontend + # - "8003:8003" # Monitoring + # - "8001:8001" # gRPC endpoint (used for replication in multi-frontend environments) + # - "8889:8889" # GUI - proxied by traefik + volumes: + - ./config/velociraptor:/config + - ./data/velociraptor:/data + - ./logs/velociraptor:/logs + - /var/lib/ca-certificates/ca-bundle.pem:/etc/ssl/certs/ca-certificates.crt:ro + labels: + traefik.enable: "true" + traefik.http.routers.frontend.rule: "Host(`${SENSOR_GUI_HOSTNAME}`)" + traefik.http.routers.frontend.entrypoints: web + traefik.http.routers.frontend.middlewares: https_redirect + traefik.http.routers.frontendtls.rule: "Host(`${SENSOR_GUI_HOSTNAME}`)" + traefik.http.routers.frontendtls.entrypoints: websecure + traefik.http.routers.frontendtls.tls: true + traefik.http.routers.frontendtls.tls.certresolver: myresolver + traefik.http.middlewares.https_redirect.redirectscheme.scheme: https + traefik.http.middlewares.https_redirect.redirectscheme.permanent: true + traefik.http.services.frontend.loadbalancer.server.port: 8889 + traefik.http.services.frontend.loadbalancer.passhostheader: true + restart: + unless-stopped + sensor-client: + image: registry.opensuse.org/security/sensor/containers/linux-security-sensor:latest + hostname: sensor-client + container_name: sensor-client + user: root + command: /usr/bin/velociraptor client --config /config/client.conf -v + depends_on: + - sensor-frontend + volumes: + - ./config/velociraptor:/config:ro + - client-data:/var/lib/velociraptor-client + restart: + unless-stopped + +networks: + default: diff --git a/SUSE/docker-compose/env.example b/SUSE/docker-compose/env.example new file mode 100644 index 000000000..9de78a766 --- /dev/null +++ b/SUSE/docker-compose/env.example @@ -0,0 +1 @@ +SENSOR_GUI_HOSTNAME= diff --git a/SUSE/docker-compose/logs/velociraptor/README.md b/SUSE/docker-compose/logs/velociraptor/README.md new file mode 100644 index 000000000..b9d3c98ed --- /dev/null +++ b/SUSE/docker-compose/logs/velociraptor/README.md @@ -0,0 +1,4 @@ +Placeholder file += + +This file is a placeholder so that the logs/velociraptor directory exists in the Git repository. diff --git a/SUSE/docker/Dockerfile b/SUSE/docker/Dockerfile new file mode 100644 index 000000000..6655f00b5 --- /dev/null +++ b/SUSE/docker/Dockerfile @@ -0,0 +1,39 @@ +# Defines the tag for OBS and build script builds: +#!BuildTag: linux-security-sensor:%PKG_VERSION%.%GIT_OFFSET% linux-security-sensor:%PKG_VERSION% linux-security-sensor:%PKG_VERSION%.%GIT_OFFSET%.%RELEASE% linux-security-sensor +FROM opensuse/leap:15.4 + +# Need to build on SLE first -- it's mostly static but depends on glibc +#FROM registry.suse.com/suse/sle15:latest + +# labelprefix=org.opensuse.linux-security-sensor +LABEL org.opencontainers.image.title="Linux Security Sensor Server Container" +LABEL org.opencontainers.image.description="This contains Linux Security Sensor %PKG_VERSION%.%GIT_OFFSET%" +LABEL org.opensuse.version="%PKG_VERSION%.%GIT_OFFSET%" +LABEL org.openbuildservice.disturl="%DISTURL%" +LABEL org.opencontainers.image.created="%BUILDTIME%" +LABEL org.opensuse.reference="registry.opensuse.org/security/sensor/containers/linux-security-sensor:%PKG_VERSION%.%GIT_OFFSET%.%RELEASE%" +# endlabelprefix + +VOLUME /data +VOLUME /logs +VOLUME /config + +# API +EXPOSE 8801 + +# GUI +EXPOSE 8889 + +# Frontend +EXPOSE 8000 + +# Monitoring +EXPOSE 8003 + +COPY entry-point.sh generate-config.sh / +COPY init-config.json /etc/velociraptor/ +RUN chmod a+x /entry-point.sh /generate-config.sh +RUN zypper -q --non-interactive install velociraptor catatonit && \ + zypper clean -a + +CMD ["/usr/bin/catatonit", "--", "/entry-point.sh" ] diff --git a/SUSE/docker/Dockerfile.devel b/SUSE/docker/Dockerfile.devel new file mode 100644 index 000000000..41178c703 --- /dev/null +++ b/SUSE/docker/Dockerfile.devel @@ -0,0 +1,25 @@ +FROM opensuse/leap:15.4 + +VOLUME /data +VOLUME /logs +VOLUME /config + +# API +EXPOSE 8801 + +# GUI +EXPOSE 8889 + +# Frontend +EXPOSE 8000 + +# Monitoring +EXPOSE 8003 + +COPY entry-point.sh generate-config.sh / +COPY init-config.json /etc/velociraptor/ +RUN chmod a+x /entry-point.sh /generate-config.sh +RUN zypper -q --non-interactive install catatonit && zypper clean -a +COPY velociraptor-v0.6.4-2-linux-amd64 /usr/bin/velociraptor + +CMD ["/usr/bin/catatonit", "--", "/entry-point.sh" ] diff --git a/SUSE/docker/Dockerfile.local b/SUSE/docker/Dockerfile.local new file mode 100644 index 000000000..da7aa7edc --- /dev/null +++ b/SUSE/docker/Dockerfile.local @@ -0,0 +1,30 @@ +FROM opensuse/leap:15.4 + +VOLUME /data +VOLUME /logs +VOLUME /config + +# API +EXPOSE 8801 + +# GUI +EXPOSE 8889 + +# Frontend +EXPOSE 8000 + +# Monitoring +EXPOSE 8003 + +COPY entry-point.sh generate-config.sh obs-signing-key.key / +COPY init-config.json /etc/velociraptor/ + +RUN chmod a+x /entry-point.sh /generate-config.sh +RUN rpm --import /obs-signing-key.key +RUN zypper -q ar obs://security:sensor/ "obs://security/sensor" && \ + zypper -q --non-interactive refresh && \ + zypper -q --non-interactive install velociraptor catatonit && \ + zypper clean -a && \ + rm -f /obs-signing-key.key + +CMD ["/usr/bin/catatonit", "--", "/entry-point.sh" ] diff --git a/SUSE/docker/_icon b/SUSE/docker/_icon new file mode 100644 index 000000000..93ba3b359 Binary files /dev/null and b/SUSE/docker/_icon differ diff --git a/SUSE/docker/_service b/SUSE/docker/_service new file mode 100644 index 000000000..b71a0bcfa --- /dev/null +++ b/SUSE/docker/_service @@ -0,0 +1,20 @@ + + + + Dockerfile + %PKG_VERSION% + patch + + velociraptor + + + Dockerfile + %GIT_OFFSET% + offset + + velociraptor + + + diff --git a/SUSE/docker/entry-point.sh b/SUSE/docker/entry-point.sh new file mode 100644 index 000000000..b2267d062 --- /dev/null +++ b/SUSE/docker/entry-point.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +SERVER_CONFIG="/config/server.conf" + +if test ! -e "${SERVER_CONFIG}"; then + echo "No config file found. Generating default at "${SERVER_CONFIG}"." >&2 + /generate-config.sh +fi + +exec velociraptor frontend -v --config "${SERVER_CONFIG}" diff --git a/SUSE/docker/generate-config.sh b/SUSE/docker/generate-config.sh new file mode 100644 index 000000000..67b010605 --- /dev/null +++ b/SUSE/docker/generate-config.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +CONFIG="/config" +MERGE_FILE="/etc/velociraptor/init-config.json" +SERVER_CONFIG="${CONFIG}/server.conf" +CLIENT_CONFIG="${CONFIG}/client.conf" + +CLIENT_DIR="/var/lib/velociraptor-client" +BUFFER_FILE="${CLIENT_DIR}/Velociraptor_Buffer.bin" +WRITEBACK_FILE="${CLIENT_DIR}/velociraptor.writeback.yaml" + +usage() { + echo "$(basename "$0") [-f]" + exit 1 +} + +force=false +while getopts f value "$@"; do + case "$value" in + f) force=true ;; + ?) usage ;; + esac +done + +shift $(( $OPTIND - 1 )) + +if test -e "${SERVER_CONFIG}" -a "$force" != "true"; then + echo "${SERVER_CONFIG} already exists. Will not replace without -f." >&2 + exit 1 +fi + +velociraptor config generate --merge_file="$MERGE_FILE" |grep -v '^ *.*{}' > "$SERVER_CONFIG" + +awk " +/^Client/ { print \$0; seen_client=1; next; } +/^[A-Za-z]/ { if (seen_client == 1) exit; } +{ if (seen_client == 1 && skip_record != 1) print \$0; } +" < "${SERVER_CONFIG}" > "${CLIENT_CONFIG}" + +sed -e 's#https://sensor-frontend:8000/#https//velociraptor.fqdn:8000/' < "${CLIENT_CONFIG}" > "${CLIENT_CONFIG}.template" diff --git a/SUSE/docker/init-config.json b/SUSE/docker/init-config.json new file mode 100644 index 000000000..3d0de1dd7 --- /dev/null +++ b/SUSE/docker/init-config.json @@ -0,0 +1,47 @@ +{ "Client" : + { "writeback_linux" : "/var/lib/velociraptor-client/velociraptor.writeback.yaml", + "writeback_darwin" : "", + "writeback_windows" : "", + "tempdir_windows" : "", + "windows_installer" : { "service_name": "", "install_path": "", "service_description" : ""}, + "darwin_installer" : { "service_name": "", "install_path": "", "service_description" : ""}, + "version" : { "name": "", "version" : "", "build_time" : "" }, + "local_buffer" : { + "filename_windows" : "", + "filename_darwin" : "", + "filename_linux" : "/var/lib/velociraptor-client/Velociraptor_Buffer.bin" + }, + "use_self_signed_ssl" : true, + "server_urls" : [ "https://sensor-frontend:8000/" ] + }, + "Datastore" : + { + "location": "/data", + "filestore_directory" : "/data" + }, + "Logging" : + { + "output_directory" : "/logs" + }, + "API": + { + "bind_address": "0.0.0.0" + }, + "Frontend": + { + "bind_address": "0.0.0.0" + }, + "GUI": + { + "bind_address": "0.0.0.0", + "initial_users" : [ { + "name" : "admin", + "password_hash" : "5c39c98764173fba7cc290553f06d631c1205b2727317e413b2e62e4a1133b60", + "password_salt" : "af1904fcca4dd3f8fa97ecf2431ecb718513b2ef361515e1830d400d23b03f3e" + }] + }, + "Monitoring": + { + "bind_address": "0.0.0.0" + } +} diff --git a/SUSE/docker/obs-signing-key.key b/SUSE/docker/obs-signing-key.key new file mode 100644 index 000000000..1e9ca3dab --- /dev/null +++ b/SUSE/docker/obs-signing-key.key @@ -0,0 +1,21 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.5 (GNU/Linux) + +mQENBFODA6oBCAC1ZlvBSl+aNBm+NH7xGiNa8JRpZ0ujQIc941ozc2T2Pqe3tscZ +Z6KjsqSwTX6jzomTgGyqJDYd308KEHeqIMuZVlCZQDsyHb6YiuOa051ice3eas94 +PVJ+z6Do9zSVOLwc0xsdy4jBdiB7K5XN3iGmmboK3oiFbNJRP0b+saFSJ3R9lQQ6 +c7iD++tFl36/ovwWitwqzJ3cYuWeGxHjBvTV4YCQb2JECQgskfloHcjqMIyevJm1 +4KNmrHn2Q12qPfrHECtnf/hP/9yrCvbekT/aLWx/IV/vIQdHJPwnYDRFDN6tyuDJ +kh4QVYYyFaKaSHNJ2it3lRMeIAzaCnSVJQ8lABEBAAG0MnNlY3VyaXR5IE9CUyBQ +cm9qZWN0IDxzZWN1cml0eUBidWlsZC5vcGVuc3VzZS5vcmc+iQE+BBMBAgAoBQJf +6TKkAhsDBQkQhN76BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBp0bKq7j0W +amJyCACo9SLaYQEpCBi/crZ6IGR7EmKn35YBQeZ7c/xZEfQja/pHLCHR1A02rUoz +cDQsr5cf2NSR9Hpbta/G9RwjICIVqUJkno3Nm2DQdJVIw8cg774kYORHP4MOq8IS +p8wTASJKm7yxJ4e/lRdl4oZCSegFVYymz+6T1KBrFasi2CwbuKzqeA/J4ma/QrXx +kvOmh1HNYhXU5YSXJugv4VcejSbPNL//59nGuKPoODJv4Yz2BoLJkjMnIu960ohv +8UDmImDwC2KlS0R61aD5dZWPbOrPuj/2fHjYmJjRks2fFl8kag5qyNTLM5o6i7CH +R0H+1bsjhIE1bjO5zyvkXG0H0LEqiEYEExECAAYFAlODA6sACgkQOzARt2udZSO5 +wgCfdn5fA8nzafycfrO4iXjg7/E34E4AnixpDpJ8llW/+4r+MC59fMWa59oj +=h018 +-----END PGP PUBLIC KEY BLOCK----- + diff --git a/accessors/acl.go b/accessors/acl.go new file mode 100644 index 000000000..63ddc7437 --- /dev/null +++ b/accessors/acl.go @@ -0,0 +1 @@ +package accessors diff --git a/accessors/api.go b/accessors/api.go new file mode 100644 index 000000000..d4d60d177 --- /dev/null +++ b/accessors/api.go @@ -0,0 +1,495 @@ +package accessors + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "sync" + "time" + + "github.com/Velocidex/ordereddict" + errors "github.com/go-errors/errors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/types" +) + +// An OS Path can be thought of as a sequence of components. The OS +// APIs receive a single string, which is the serialization of the OS +// Path. Different operating systems have different serialization +// methods to arrive at the same OS Path. + +// For example, on Windows components are separated by the backslash +// characted and the first component can be a device name (which may +// contain path separators): + +// \\.\C:\Windows\System32 -> ["\\.\C:", "Windows", "System32"] +// C:\Windows\System32 -> ["C:", "Windows", "System32"] + +// On Linux, the path separator is / and serializations start with "/" + +// /usr/bin/ls -> ["usr", "bin", "ls"] + +// In Velociraptor we try to keep the OS Path object intact as far as +// possible and only serialize to OS representation when necessary. + +type PathManipulator interface { + PathParse(path string, result *OSPath) error + PathJoin(path *OSPath) string + AsPathSpec(path *OSPath) *PathSpec + ComponentEqual(a, b string) bool +} + +type OSPath struct { + mu sync.Mutex + + Components []string + + // Some paths need more information. They store an additional path + // spec here. + pathspec *PathSpec + serialized *string + Manipulator PathManipulator + + // Opaque data that can be stored in the OSPath. This provides a + // mechanism to transport additional data in the OSPath and avoid + // having to convert back and forth. + Data interface{} +} + +func (self *OSPath) Equal(other *OSPath) bool { + if !utils.StringSliceEq(self.Components, other.Components) { + return false + } + + return self.String() == other.String() +} + +func (self *OSPath) DescribeType() string { + subtype := "" + switch self.Manipulator.(type) { + case LinuxPathManipulator: + subtype = "LinuxPath" + case GenericPathManipulator: + subtype = "Generic" + case WindowsPathManipulator: + subtype = "WindowsPath" + case WindowsNTFSManipulator: + subtype = "NTFSPath" + case WindowsRegistryPathManipulator: + subtype = "RegistryPath" + case PathSpecPathManipulator: + subtype = "PathSpec" + case FileStorePathManipulator: + subtype = "FileStorePath" + case RawFileManipulator: + subtype = "RawPath" + case ZipFileManipulator: + subtype = "ZipPathspec" + default: + subtype = fmt.Sprintf("%T", self.Manipulator) + } + return fmt.Sprintf("OSPath(%s)", subtype) +} + +// Make a copy of the OSPath +func (self *OSPath) Copy() *OSPath { + self.mu.Lock() + defer self.mu.Unlock() + + pathspec := self.pathspec + if pathspec != nil { + pathspec = pathspec.Copy() + } + return &OSPath{ + Components: utils.CopySlice(self.Components), + pathspec: pathspec, + Manipulator: self.Manipulator, + } +} + +func (self *OSPath) SetPathSpec(pathspec *PathSpec) error { + self.mu.Lock() + defer self.mu.Unlock() + + err := self.Manipulator.PathParse(pathspec.Path, self) + if err != nil { + return err + } + self.pathspec = pathspec + return nil +} + +func (self *OSPath) PathSpec() *PathSpec { + self.mu.Lock() + defer self.mu.Unlock() + + return self.Manipulator.AsPathSpec(self) +} + +func (self *OSPath) DelegatePath() string { + self.mu.Lock() + defer self.mu.Unlock() + + pathspec := self.Manipulator.AsPathSpec(self) + if pathspec.DelegatePath == "" && pathspec.Delegate != nil { + pathspec.DelegatePath = json.MustMarshalString(pathspec.Delegate) + } + return pathspec.DelegatePath +} + +func (self *OSPath) DelegateAccessor() string { + self.mu.Lock() + defer self.mu.Unlock() + + return self.Manipulator.AsPathSpec(self).DelegateAccessor +} + +func (self *OSPath) Path() string { + self.mu.Lock() + defer self.mu.Unlock() + + return self.Manipulator.AsPathSpec(self).Path +} + +func (self *OSPath) String() string { + self.mu.Lock() + defer self.mu.Unlock() + + // Cache it if we need to. + if self.serialized != nil { + return *self.serialized + } + + res := self.Manipulator.PathJoin(self) + self.serialized = &res + + return res +} + +func (self *OSPath) Parse(path string) (*OSPath, error) { + self.mu.Lock() + defer self.mu.Unlock() + + result := &OSPath{ + Manipulator: self.Manipulator, + } + + err := self.Manipulator.PathParse(path, result) + return result, err +} + +func (self *OSPath) Basename() string { + self.mu.Lock() + defer self.mu.Unlock() + + if len(self.Components) > 0 { + return self.Components[len(self.Components)-1] + } + return "" +} + +func (self *OSPath) Dirname() *OSPath { + result := self.Copy() + if len(result.Components) > 0 { + result.Components = result.Components[:len(self.Components)-1] + } + return result +} + +// TrimComponents removes the specified components from the start of +// our own components. +// For example if self = ["C:", "Windows", "System32"] +// then TrimComponents("C:") -> ["Windows", "System32"] +func (self *OSPath) TrimComponents(components ...string) *OSPath { + if components == nil { + return self.Copy() + } + + result := self.Copy() + for idx, c := range result.Components { + if idx >= len(components) || + !self.Manipulator.ComponentEqual(c, components[idx]) { + result := &OSPath{ + Components: utils.CopySlice(self.Components[idx:]), + pathspec: self.pathspec, + Manipulator: self.Manipulator, + } + return result + } + } + result.Components = nil + return result +} + +// Does the path has the required prefix? +func (self *OSPath) HasPrefix(components ...string) bool { + if len(self.Components) > len(components) { + return false + } + + for idx, c := range components { + if !self.Manipulator.ComponentEqual(c, self.Components[idx]) { + return false + } + } + + return true +} + +// Produce a human readable string - this is a one way conversion: It +// is not possible to go back to a proper OSPath from this. +func (self *OSPath) HumanString(scope types.Scope) string { + result := []string{self.Path()} + delegate := self + + for { + next_delegate, err := delegate.Delegate(scope) + if err != nil { + break + } + delegate = next_delegate + if len(delegate.Components) == 0 { + break + } + result = append(result, delegate.Path()) + } + + // Reverse the slice + for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { + result[i], result[j] = result[j], result[i] + } + + // As an alternative form we can use maybe? return path.Join(result...) + return strings.Join(result, " -> ") +} + +// Make a copy +func (self *OSPath) Append(children ...string) *OSPath { + result := self.Copy() + result.Components = append(result.Components, children...) + + return result +} + +func (self *OSPath) Clear() *OSPath { + return &OSPath{ + Manipulator: self.Manipulator, + } +} + +func (self *OSPath) Delegate(scope vfilter.Scope) (*OSPath, error) { + accessor, err := GetAccessor(self.DelegateAccessor(), scope) + if err != nil { + return nil, err + } + + return accessor.ParsePath(self.DelegatePath()) +} + +func (self *OSPath) MarshalJSON() ([]byte, error) { + return json.Marshal(self.String()) +} + +func (self *OSPath) MarshalYAML() (interface{}, error) { + json_string := []byte(self.String()) + buf := bytes.Buffer{} + err := json.Indent(&buf, json_string, " ", " ") + return string(buf.Bytes()), err +} + +// MarshalText is used by the YAML marshaller. We indent the text to +// make sure it uses multi line yaml which is more readable for +// complex pathspecs. +func (self *OSPath) MarshalText() ([]byte, error) { + json_string := []byte(self.String()) + buf := bytes.Buffer{} + err := json.Indent(&buf, json_string, " ", " ") + return buf.Bytes(), err +} + +// A FileInfo represents information about a file. It is similar to +// os.FileInfo but not identical. +type FileInfo interface { + Name() string + ModTime() time.Time + + // Path as OS serialization. + FullPath() string + + OSPath() *OSPath + + // Time the file was birthed (initially created) + Btime() time.Time + Mtime() time.Time + + // Time the inode was changed. + Ctime() time.Time + Atime() time.Time + + // Arbitrary key/value for storing file metadata. This is accessor + // dependent can be nil. + Data() *ordereddict.Dict + Size() int64 + + IsDir() bool + IsLink() bool + GetLink() (*OSPath, error) + Mode() os.FileMode +} + +// Some filesystems return multiple files with the same basename. They +// should implement this interface so we can properly dedup based on a +// unique name. +type UniqueBasename interface { + UniqueName() string +} + +// A File reader with +type ReadSeekCloser interface { + io.ReadSeeker + io.Closer +} + +// Some files are not really seekable (although they may pretend to +// be). Sometimes it is important to know if the file may be rewound +// back if we read from it - before we actually read from it. If a +// ReadSeekCloser also implements the Seekable interface it may report +// if it can be seeked. +type Seekable interface { + IsSeekable() bool +} + +func IsSeekable(fd ReadSeekCloser) bool { + seekable, ok := fd.(Seekable) + if ok { + return seekable.IsSeekable() + } + + return true +} + +// Interface for accessing the filesystem. +type FileSystemAccessor interface { + // List a directory. + ReadDir(path string) ([]FileInfo, error) + + // Open a file for reading + Open(path string) (ReadSeekCloser, error) + Lstat(filename string) (FileInfo, error) + + // Converts from a string path to an OSPath suitable for this + // accessor. + ParsePath(filename string) (*OSPath, error) + + // The new more efficient API + ReadDirWithOSPath(path *OSPath) ([]FileInfo, error) + OpenWithOSPath(path *OSPath) (ReadSeekCloser, error) + LstatWithOSPath(path *OSPath) (FileInfo, error) + New(scope vfilter.Scope) (FileSystemAccessor, error) + + Describe() *AccessorDescriptor +} + +// Some filesystems can attempt to retrieve the underlying file. If +// this interface exists on the accessor **and** the +// GetUnderlyingAPIFilename() call succeeds, then it should be +// possible to directly access the returned filename using the OS +// APIs. +type RawFileAPIAccessor interface { + GetUnderlyingAPIFilename(path *OSPath) (string, error) +} + +var ( + NotRawFileSystem = errors.New("NotRawFileSystem") +) + +func GetUnderlyingAPIFilename(accessor string, + scope vfilter.Scope, path *OSPath) (string, error) { + accessor_obj, err := GetAccessor(accessor, scope) + if err != nil { + return "", err + } + + raw_accessor, ok := accessor_obj.(RawFileAPIAccessor) + if !ok { + return "", NotRawFileSystem + } + + return raw_accessor.GetUnderlyingAPIFilename(path) +} + +// For case insensitive filesystems, the canonical filename (used in +// comparisons) can be different from the actual filename. +type CanonicalFilenameAccessor interface { + GetCanonicalFilename(path *OSPath) string +} + +func GetCanonicalFilename(accessor string, + scope vfilter.Scope, path *OSPath) string { + accessor_obj, err := GetAccessor(accessor, scope) + if err != nil { + return path.String() + } + + raw_accessor, ok := accessor_obj.(CanonicalFilenameAccessor) + if !ok { + return path.String() + } + + return raw_accessor.GetCanonicalFilename(path) +} + +type AccessorDescriptor struct { + Name string + Description string + + // The required permissions for using this accessor + Permissions []acls.ACL_PERMISSION + + // The name of the scope parameter that configures this accessor + // if needed. + ScopeVar string + + // The type description for the ScopeVar if present. + ArgType vfilter.Any +} + +func (self AccessorDescriptor) Metadata() *ordereddict.Dict { + var permissions []string + for _, p := range self.Permissions { + permissions = append(permissions, p.String()) + } + + res := ordereddict.NewDict() + if len(permissions) > 0 { + res.Set("permissions", strings.Join(permissions, ",")) + } + + if self.ScopeVar != "" { + res.Set("ScopeVar", self.ScopeVar) + } + + return res +} + +type DescriptorWrapper struct { + FileSystemAccessor + descriptor AccessorDescriptor +} + +func (self DescriptorWrapper) Describe() *AccessorDescriptor { + return &self.descriptor +} + +func DescribeAccessor(target FileSystemAccessor, + desc AccessorDescriptor) FileSystemAccessor { + return DescriptorWrapper{ + FileSystemAccessor: target, + descriptor: desc, + } +} diff --git a/accessors/api_test.go b/accessors/api_test.go new file mode 100644 index 000000000..1b2d63f06 --- /dev/null +++ b/accessors/api_test.go @@ -0,0 +1,168 @@ +package accessors_test + +import ( + "strings" + "testing" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/zip" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/json" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" + + _ "www.velocidex.com/golang/velociraptor/accessors/ntfs" + _ "www.velocidex.com/golang/velociraptor/accessors/offset" +) + +type api_tests struct { + name string + path string + components string +} + +var ( + // Trim the prefix from a path + trim_tests = []api_tests{ + {"Simple Path", + "C:/Windows/System32", "C:"}, + + {"Simple Path Deep", + "C:/Windows/System32", "C:,Windows"}, + + {"Complex Pathspec", + `{ + "DelegateAccessor": "raw_ntfs", + "Delegate":{ + "DelegateAccessor": "file", + "DelegatePath":"/mnt/flat", + "Path":"/Windows/System32/Config/SYSTEM" + }, + "Path":"ControlSet001" +}`, + "ControlSet001"}, + + // Trim just one prefix directory. + {"Complex Pathspec Deep", + `{ + "DelegateAccessor": "raw_ntfs", + "Delegate":{ + "DelegateAccessor": "file", + "DelegatePath":"/mnt/flat", + "Path":"/Windows/System32/Config/SYSTEM" + }, + "Path":"ControlSet001/Foo/Bar" +}`, + "ControlSet001"}, + } + + append_tests = []api_tests{ + {"Simple Path", + "C:/Windows/", "System32,notepad.exe"}, + + {"Complex Pathspec", + `{ + "DelegateAccessor": "raw_ntfs", + "Delegate":{ + "DelegateAccessor": "file", + "DelegatePath":"/mnt/flat", + "Path":"/Windows/System32/Config/SYSTEM" + }, + "Path":"ControlSet001" +}`, + "Foo,Bar"}, + } +) + +// Make sure OSPath can handle complex path manipulations +func TestOSPathOperationsTrimComponents(t *testing.T) { + result := ordereddict.NewDict() + for _, test_case := range trim_tests { + a := accessors.MustNewWindowsOSPath(test_case.path) + components := strings.Split(test_case.components, ",") + trimmed := a.TrimComponents(components...) + result.Set(test_case.name, trimmed) + } + + goldie.Assert(t, "TestOSPathOperationsTrimComponents", + json.MustMarshalIndent(result)) +} + +func TestOSPathOperationsAppendComponents(t *testing.T) { + result := ordereddict.NewDict() + for _, test_case := range append_tests { + a := accessors.MustNewWindowsOSPath(test_case.path) + components := strings.Split(test_case.components, ",") + trimmed := a.Append(components...) + result.Set(test_case.name, trimmed) + } + + goldie.Assert(t, "TestOSPathOperationsAppendComponents", + json.MustMarshalIndent(result)) +} + +type human_string_tests_t struct { + name string + pathspec string + path_type string +} + +var human_string_tests = []human_string_tests_t{ + {"Deep Pathspec", + `{ + "Path": "/ControlSet001", + "DelegateAccessor": "raw_ntfs", + "Delegate": { + "DelegateAccessor":"offset", + "Delegate": { + "DelegateAccessor": "virt", + "DelegatePath": "/shared/mnt/flat", + "Path": "122683392" + }, + "Path":"/Windows/System32/Config/SYSTEM" + } + } +`, "linux"}, + {"Normal path", `C:\Windows\System32`, "windows"}, +} + +func TestOSPathHumanString(t *testing.T) { + config_obj := &config_proto.Config{} + + // To make this test run on Linux and Windows the same we use a + // neutral accessor. + device_manager := accessors.GetDefaultDeviceManager(config_obj).Copy() + device_manager.Register(accessors.DescribeAccessor( + accessors.NewVirtualFilesystemAccessor(accessors.MustNewLinuxOSPath("")), + accessors.AccessorDescriptor{ + Name: "virt", + })) + + scope := vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{}). + Set(constants.SCOPE_DEVICE_MANAGER, device_manager)) + + result := ordereddict.NewDict() + for _, test_case := range human_string_tests { + switch test_case.path_type { + case "linux": + a := accessors.MustNewLinuxOSPath(test_case.pathspec) + result.Set(test_case.name, a.HumanString(scope)) + + case "windows": + a := accessors.MustNewWindowsOSPath(test_case.pathspec) + result.Set(test_case.name, a.HumanString(scope)) + } + } + goldie.Assert(t, "TestOSPathHumanString", + json.MustMarshalIndent(result)) +} + +func init() { + // Override the file accessor with something that uses Generic + // ospath so tests are the same on windows and linux. + accessors.Register(&zip.ZipFileSystemAccessor{}) +} diff --git a/accessors/collector/collector.go b/accessors/collector/collector.go new file mode 100644 index 000000000..cb6eea195 --- /dev/null +++ b/accessors/collector/collector.go @@ -0,0 +1,472 @@ +package collector + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/zip" + "www.velocidex.com/golang/velociraptor/acls" + actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/uploads" + "www.velocidex.com/golang/velociraptor/utils" + + crypto_utils "www.velocidex.com/golang/velociraptor/crypto/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +// An accessor for reading collector containers. The Offline collector +// is designed to store files in a zip container. However the zip +// format is not capable of storing certain attributes (e.g. sparse +// files). The accessor is designed to read the offline collector +// containers. + +// This accessor wraps the zip accessor to provide access to these +// specially formated conatiners. In particular the collector accessor +// handles the following two properties transparently: + +// 1. Zip encryption: Velociraptor uses an ecnryption scheme to work +// around Zip encryption limitations. All data is stored in an +// encrypted file called "data.zip" inside the main zip archive. This +// is because Zip encryption does not protect the central directory or +// the filenames - only data content. + +// 2. Public Key Encryption: Velociraptor stored metadata in a +// "metadata.json" file containing the encrypted session key. This +// allows private/public key encryption and transparent decryption. + +// This accessor facilitates this transparent decryption. + +type rangedReader struct { + delegate io.ReaderAt + fd accessors.ReadSeekCloser + offset int64 +} + +func (self *rangedReader) Read(buff []byte) (int, error) { + n, err := self.delegate.ReadAt(buff, self.offset) + self.offset += int64(n) + return n, err +} + +func (self *rangedReader) Seek(offset int64, whence int) (int64, error) { + self.offset = offset + return self.offset, nil +} + +func (self *rangedReader) Close() error { + return self.fd.Close() +} + +type StatWrapper struct { + accessors.FileInfo + real_size int64 +} + +func (self StatWrapper) Size() int64 { + return self.real_size +} + +func (self StatWrapper) Mode() os.FileMode { + if self.real_size == 0 { + return os.FileMode(0755) + } + return self.FileInfo.Mode() +} + +func (self StatWrapper) IsDir() bool { + return self.real_size == 0 +} + +type CollectorAccessor struct { + *zip.ZipFileSystemAccessor + scope vfilter.Scope + + // If set we automatically pad out sparse files. + expandSparse bool +} + +func (self *CollectorAccessor) New(scope vfilter.Scope) (accessors.FileSystemAccessor, error) { + delegate, err := accessors.GetAccessor("zip_nocase", scope) + if err != nil { + return nil, err + } + return &CollectorAccessor{ + expandSparse: self.expandSparse, + ZipFileSystemAccessor: delegate.(*zip.ZipFileSystemAccessor), + scope: scope, + }, err +} + +/* + Go from a pathspec like: + + PathSpec{ + Path: "path/within/zip", + DelegatePath: "/path/to/zip/collection", + DelegateAccessor: "accssor_to_zip_collection", + } + + To a pathspec like + PathSpec{ + Path: "path/within/zip", + DelegateAccessor: "collector", + DelegatePath: PathSpec{ + Path: "data.zip", + DelegatePath: "/path/to/zip/collection", + DelegateAccessor: "accssor_to_zip_collection", + }, + } + +*/ + +func collectorPathToDelegatePath(full_path *accessors.OSPath) *accessors.OSPath { + // Detect an already transformed path and leave it alone. + if len(full_path.Components) == 1 && + full_path.Components[0] == "data.zip" { + return full_path + } + + if full_path.DelegateAccessor() == "collector" { + return full_path + } + + collector_pathspec := full_path.PathSpec() + + res := full_path.Copy() + _ = res.SetPathSpec(&accessors.PathSpec{ + Path: collector_pathspec.Path, + DelegateAccessor: "collector", + DelegatePath: accessors.PathSpec{ + DelegateAccessor: collector_pathspec.DelegateAccessor, + DelegatePath: collector_pathspec.DelegatePath, + Path: "data.zip", + }.String(), + }) + res.Components = full_path.Components + + return res +} + +// Attempt to extract the password from the container. +func ExtractPassword( + scope vfilter.Scope, + accessor accessors.FileSystemAccessor, + full_path *accessors.OSPath) (string, error) { + + // Check if data.zip exists at the top level. + root := full_path.Copy() + root.Components = nil + + datazip := root.Append("data.zip") + _, err := accessor.LstatWithOSPath(datazip) + if err != nil { + // Nope - no data.zip so do not transform the pathspec. + return "", err + } + + // Check if metadata.json exists. If so, try to extract password + meta := root.Append("metadata.json") + mhandle, err := accessor.OpenWithOSPath(meta) + if err != nil { + // No metadata file is found - this might be a plain + // collection zip. + return "", err + } + + buf, err := utils.ReadAllWithLimit(mhandle, constants.MAX_MEMORY) + if err != nil { + return "", fmt.Errorf("Decoding metadata.json: %w", err) + } + + rows := []*ordereddict.Dict{} + err = json.Unmarshal(buf, &rows) + if err != nil { + return "", fmt.Errorf("Decoding metadata.json: %w", err) + } + + // metadata.json can be multiple rows + for _, row := range rows { + scheme, ok := row.GetString("Scheme") + if !ok { + // Maybe multiple rows? + continue + } + + if strings.ToLower(scheme) == "x509" { + ep, ok := row.GetString("EncryptedPass") + if !ok { + return "", errors.New( + "EncryptedPass must be given and be of type string!") + } + + err = vql_subsystem.CheckAccess(scope, acls.SERVER_ADMIN) + if err != nil { + return "", errors.New( + "Must be server admin to use private key") + } + + key, err := crypto_utils.GetPrivateKeyFromScope(scope) + if err != nil { + return "", fmt.Errorf("GetPrivateKeyFromScope: %w", err) + } + + zip_pass, err := crypto_utils.Base64DecryptRSAOAEP(key, ep) + if err != nil { + return "", fmt.Errorf("Unable to extract zip password: %w", err) + } + + // Transform the path so it can be used by the zip + // collector. + return string(zip_pass), nil + } + } + + return "", utils.NotFoundError +} + +// Try to set a password if it exists in metadata +func (self *CollectorAccessor) maybeSetZipPassword( + full_path *accessors.OSPath) (*accessors.OSPath, error) { + + // If password is already set in the scope, just use it as it is. + pass, pres := self.scope.Resolve(constants.ZIP_PASSWORDS) + if pres && !utils.IsNil(pass) { + if utils.ToString(pass) != "" { + return collectorPathToDelegatePath(full_path), nil + } + } else { + // Password is already cached in the context - just return it as is. + pass, pres = self.scope.GetContext(constants.ZIP_PASSWORDS) + if pres && !utils.IsNil(pass) { + + // Transform the path so it is ready to be used by the zip + // accessor. + return collectorPathToDelegatePath(full_path), nil + } + } + + zip_pass, err := ExtractPassword(self.scope, + self.ZipFileSystemAccessor, full_path) + if err == nil { + // Record the password in the scope so next time we can + // automatically use it. + self.scope.SetContext(constants.ZIP_PASSWORDS, string(zip_pass)) + + value, pres := self.scope.Resolve(constants.REPORT_ZIP_PASSWORD) + if pres && self.scope.Bool(value) { + self.scope.Log( + "CollectorAccessor: X509 Decrypted password is %q", + string(zip_pass)) + } + + return collectorPathToDelegatePath(full_path), nil + } + + // Report serious errors + if !utils.IsNotFound(err) { + return nil, err + } + + // No metadata found - this might be a plain unencrypted + // collection. + return full_path, nil +} + +// Zip files typically use standard / path separators. +func (self *CollectorAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewZipFilePath(path) +} + +func (self *CollectorAccessor) Open( + filename string) (accessors.ReadSeekCloser, error) { + + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self *CollectorAccessor) getIndex( + full_path *accessors.OSPath) (*actions_proto.Index, error) { + + // Does the file have an idx file? + idx_reader, err := self.ZipFileSystemAccessor.OpenWithOSPath( + full_path.Dirname().Append(full_path.Basename() + ".idx")) + if err != nil { + return nil, err + } + + serialized, err := utils.ReadAllWithLimit(idx_reader, + constants.MAX_MEMORY) + if err != nil { + return nil, err + } + + index := &actions_proto.Index{} + err = json.Unmarshal(serialized, index) + if err != nil { + // Older versions stored idx as a JSONL file instead. + for _, l := range strings.Split(string(serialized), "\n") { + if len(l) > 2 { + r := &actions_proto.Range{} + err = json.Unmarshal([]byte(l), r) + if err != nil { + return nil, err + } + index.Ranges = append(index.Ranges, r) + } + } + } + + if len(index.Ranges) == 0 { + return nil, errors.New("No ranges") + } + + return index, err +} + +func (self *CollectorAccessor) OpenWithOSPath( + full_path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + updated_full_path, err := self.maybeSetZipPassword(full_path) + if err != nil { + self.scope.Log(err.Error()) + return nil, err + } + + reader, err := self.ZipFileSystemAccessor.OpenWithOSPath(updated_full_path) + if err != nil { + return nil, err + } + + if self.expandSparse { + index, err := self.getIndex(updated_full_path) + if err == nil { + config_obj, ok := vql_subsystem.GetServerConfig(self.scope) + if !ok { + config_obj = &config_proto.Config{} + } + + if !uploads.ShouldPadFile(config_obj, index) { + self.scope.Log("Error: File %v is too sparse - unable to expand it.", full_path) + return reader, nil + } + + return &rangedReader{ + delegate: &utils.RangedReader{ + ReaderAt: utils.MakeReaderAtter(reader), + Index: index, + }, + fd: reader, + }, nil + } + } + return reader, nil +} + +func (self *CollectorAccessor) Lstat(file_path string) ( + accessors.FileInfo, error) { + full_path, err := self.ParsePath(file_path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +func (self *CollectorAccessor) LstatWithOSPath( + full_path *accessors.OSPath) (accessors.FileInfo, error) { + + updated_full_path, err := self.maybeSetZipPassword(full_path) + if err != nil { + self.scope.Log(err.Error()) + updated_full_path = full_path + } + + stat, err := self.ZipFileSystemAccessor.LstatWithOSPath(updated_full_path) + if err != nil { + return nil, err + } + + index, err1 := self.getIndex(updated_full_path) + if err1 == nil { + real_size := int64(0) + for _, r := range index.Ranges { + real_size = r.OriginalOffset + r.Length + } + + return StatWrapper{ + FileInfo: stat, + real_size: real_size, + }, nil + } + + return stat, err +} + +func (self *CollectorAccessor) ReadDir( + file_path string) ([]accessors.FileInfo, error) { + + full_path, err := self.ParsePath(file_path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(full_path) +} + +func (self *CollectorAccessor) ReadDirWithOSPath( + full_path *accessors.OSPath) ([]accessors.FileInfo, error) { + + updated_full_path, err := self.maybeSetZipPassword(full_path) + if err != nil { + return nil, err + } + + res, err := self.ZipFileSystemAccessor.ReadDirWithOSPath( + updated_full_path) + if err != nil { + return nil, err + } + + for i := range res { + res[i] = StatWrapper{ + FileInfo: res[i], + real_size: res[i].Size(), + } + } + + return res, nil +} + +func (self CollectorAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "collector", + Description: `Open a collector zip file as if it was a directory - automatically expand sparse files.`, + } +} + +func init() { + accessors.Register(&CollectorAccessor{ + expandSparse: true, + }) + + accessors.Register(accessors.DescribeAccessor( + &CollectorAccessor{ + expandSparse: false, + }, accessors.AccessorDescriptor{ + Name: "collector_sparse", + Description: `Open a collector zip file as if it was a directory - does not expand sparse files.`, + })) +} diff --git a/accessors/collector/collector_test.go b/accessors/collector/collector_test.go new file mode 100644 index 000000000..bd373ea74 --- /dev/null +++ b/accessors/collector/collector_test.go @@ -0,0 +1,118 @@ +package collector_test + +import ( + "path/filepath" + "testing" + + "github.com/Velocidex/ordereddict" + "github.com/stretchr/testify/suite" + "www.velocidex.com/golang/velociraptor/file_store/test_utils" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vql/filesystem" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" + "www.velocidex.com/golang/vfilter" + + _ "www.velocidex.com/golang/velociraptor/accessors/file" + _ "www.velocidex.com/golang/velociraptor/accessors/ntfs" +) + +const ( + TestFrontendCertificate = `-----BEGIN CERTIFICATE----- +MIIDWTCCAkGgAwIBAgIQcyUFy1oMUr4O4sIOhom/jDANBgkqhkiG9w0BAQsFADAa +MRgwFgYDVQQKEw9WZWxvY2lyYXB0b3IgQ0EwIBcNMjMwNDEzMTgzMjUzWhgPMjEy +MzAzMjAxODMyNTNaMDQxFTATBgNVBAoTDFZlbG9jaXJhcHRvcjEbMBkGA1UEAxMS +VmVsb2NpcmFwdG9yU2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9MSMbrFjmZs9bnpkel4vTQIyf+6Bpg60ByC7d6WWfBwvHdF1Qnfn1JO3Xo6p +53I1jPoagt0cZCzd6nwJXJ/3pclprmIOEBSc20pg5E0A/kpwn+bBoPNSrMF7+2/t +DvXP0Lvs/1OqUMjF8pCs6vnSKigaptn+0Et3GpzWjwCghqPcJBOuEuPQmR3HyHfs +dsMooCjuYcRcS9MXioT97SSjxeug0oTXHaKCnQ7txoxuN2+nNdr03mUu07TOUbRp +X3NsiaoESl/9IDC/tz2XTBD3UxLze9pX9t4tdKEMK2+gdnrnioOw1D7WBoElECj9 ++89CRXlu3K15P1cNVB5htPzOgwIDAQABo38wfTAOBgNVHQ8BAf8EBAMCBaAwHQYD +VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHwYDVR0j +BBgwFoAUO2IRSDwqgkZt5pkXdScs5BjoULEwHQYDVR0RBBYwFIISVmVsb2NpcmFw +dG9yU2VydmVyMA0GCSqGSIb3DQEBCwUAA4IBAQAhwcTMIdHqeR3FXOUREGjkjzC9 +vz+hPdXB6w9CMYDOAsmQojuo09h84xt7jD0iqs/K1WJpLSNV3FG5C0TQXa3PD1l3 +SsD5p4FfuqFACbPkm/oy+NA7E/0BZazC7iaZYjQw7a8FUx/P+eKo1S7z7Iq8HfmJ +yus5NlnoLmqb/3nZ7DyRWSo9HApmMdNjB6oJWrupSJajsw4Lsos2aJjkfzkg82W7 +aGSh9S6Icn1f78BAjJVLv1QBNlb+yGOhrcUWQHERPEpkb1oZJwkVVE1XCZ1C4tVj +PtlBbpcpPHB/R5elxfo+We6vmC8+8XBlNPFFp8LAAile4uQPVQjqy7k/MZ4W +-----END CERTIFICATE-----` + TestFrontendPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA9MSMbrFjmZs9bnpkel4vTQIyf+6Bpg60ByC7d6WWfBwvHdF1 +Qnfn1JO3Xo6p53I1jPoagt0cZCzd6nwJXJ/3pclprmIOEBSc20pg5E0A/kpwn+bB +oPNSrMF7+2/tDvXP0Lvs/1OqUMjF8pCs6vnSKigaptn+0Et3GpzWjwCghqPcJBOu +EuPQmR3HyHfsdsMooCjuYcRcS9MXioT97SSjxeug0oTXHaKCnQ7txoxuN2+nNdr0 +3mUu07TOUbRpX3NsiaoESl/9IDC/tz2XTBD3UxLze9pX9t4tdKEMK2+gdnrnioOw +1D7WBoElECj9+89CRXlu3K15P1cNVB5htPzOgwIDAQABAoIBAGAAy3gLOZ6hBgpU +FR7t3C2fRAFrogxozfHRw9Xc69ZIE67lXdGxSAvX2F9NI5T09c4Stt1HLoCYHH6B +Igbjc3XiNwI/0XY7L37PgItrLI2Q0vXUw3OGnJHH3gIz10472cPsQbuvrCi9Zu6K +ElijnewNCM8Sx+AZCWE1zO4P9+Z2kF9LvWzDwAa643jQ/Dg+S68zCFqjJCVJBGm+ +LQxDs6dbArvOiEbuZs2wDt0d1kZF+BRljUTMoCpdf3jmFj3f0Jc1AFaz1eHG9Gte +XIUpbWmV2ATABSW2kDkVdXx+m/w1r9PZCLLfq54fIOlm2IeAiM3rDmM4ZSTUYEPn +mJP03xECgYEA+jS7DiS3bB/MeD+5qsgS07qJhOrX17s/SlamC1dQqz+koJLl98JX +CqyafFmdSz7PK2S2+OOazngwx26Kc3MZFoD9IQ2tuWmwDgbY8EQs5Cs37By2YRZJ +DdjvVf48pCKiXxIhvFjW/5CTemNAAu4CXg5Lkp7UVVrOmf5BmjMmE0sCgYEA+m+U +QMF0f7KLM4MU81yAMJdG4Sq4s9i4RmXes2FOUd4UoG7vEpycMKkmEaqiUVmRHPjp +P6Dwq3CK+FVFMpCeWjn6KkxwpdWWO9lglI0npFcPNW/PzPOv4mSNtCAcpHrKFP0R +3jbc8UhgtFxDZoeUih7cO2iTO7kELBCeKUzw9qkCgYBgVYcj1e0tWzztm5OP9sKQ +9MRYAdei/zxKEfySZ0bu+G0ZShXzA8dhm71LXXGbdA5t5bQxNej3z/zv/FagRtOE +/5r2a/7UYaXgcLB8KbOjEiTQ6ukpjlwIUdssn9uXUqJzulZ03zvAYFj4CVivCBav +Qg/E3xRf3LupPOTjSwhA6wKBgQDAH3tnlkHueSWLNiOLc0owfM12jhS2fCsabqpD +iQHRkoLWdWRZLeYw+oLnCLWPnRvTUy11j90yWJt0Wc5FNWcWJuZBLvU4c7vWXDRY +olVoIRXc09NiEwy6rJN9PSlcEYsYQPFFPWeQfwsZMrLOZHLS50vjE53oMk7+Ex2S +56DwSQKBgQC+iHbsbxloZjVMy01V21Sh9RwIpYrodEmwlTZf2jzaYloPadHu4MX1 +jHG+zzeC/EJ3wFOKTSJ/Tmjo6N3Xaq9V7WeL8eBdtBtPztqN1yveTt94mZZ+fuID +BhI8P2RbNR2Yey5nnhFQcoTxpmVw3EYwE01nkxoPJRs/QVvxi9Mepg== +-----END RSA PRIVATE KEY-----` +) + +type TestSuite struct { + test_utils.TestSuite +} + +func (self *TestSuite) SetupTest() { + self.ConfigObj = self.LoadConfig() + self.ConfigObj.Frontend.Certificate = TestFrontendCertificate + self.ConfigObj.Frontend.PrivateKey = TestFrontendPrivateKey + + self.TestSuite.SetupTest() +} + +func (self *TestSuite) TestAutomaticDecryption() { + manager, _ := services.GetRepositoryManager(self.ConfigObj) + + builder := services.ScopeBuilder{ + Config: self.ConfigObj, + ACLManager: acl_managers.NullACLManager{}, + Logger: logging.NewPlainLogger(self.ConfigObj, &logging.FrontendComponent), + Env: ordereddict.NewDict(), + } + + scope := manager.BuildScope(builder) + + fixture_path, _ := filepath.Abs( + "../../vql/tools/collector/fixtures/offline_encrypted.zip") + + root_path_spec := (filesystem.PathSpecFunction{}).Call(self.Ctx, scope, + ordereddict.NewDict().Set("DelegatePath", fixture_path)) + + lines := []vfilter.Row{} + for row := range (filesystem.GlobPlugin{}).Call(self.Ctx, + scope, ordereddict.NewDict(). + Set("globs", "**"). + Set("accessor", "collector"). + Set("root", root_path_spec)) { + + full_path, _ := scope.Associative(row, "OSPath") + full_path_path, _ := scope.Associative(full_path, "Path") + lines = append(lines, full_path_path) + } + + goldie.AssertJson(self.T(), "TestAutomaticDecryption", lines) +} + +func TestCollectorAccessor(t *testing.T) { + suite.Run(t, &TestSuite{}) +} diff --git a/accessors/collector/fixtures/TestAutomaticDecryption.golden b/accessors/collector/fixtures/TestAutomaticDecryption.golden new file mode 100644 index 000000000..4c64bf03d --- /dev/null +++ b/accessors/collector/fixtures/TestAutomaticDecryption.golden @@ -0,0 +1,9 @@ +[ + "/collection_context.json", + "/log.json", + "/log.json.index", + "/requests.json", + "/results", + "/results/Demo.Plugins.GUI.json", + "/results/Demo.Plugins.GUI.json.index" +] \ No newline at end of file diff --git a/accessors/data/data.go b/accessors/data/data.go new file mode 100644 index 000000000..cd384d1ef --- /dev/null +++ b/accessors/data/data.go @@ -0,0 +1,97 @@ +/* + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +// A data filesystem accessor - allows data to be read as a file. + +package data + +import ( + "strings" + + "github.com/go-errors/errors" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/vfilter" +) + +type DataFilesystemAccessor struct{} + +func (self DataFilesystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "data", + Description: `Makes a string appears as an in memory file. Path is taken as a literal string to use as the file's data`, + } +} + +func (self DataFilesystemAccessor) New( + scope vfilter.Scope) (accessors.FileSystemAccessor, error) { + return DataFilesystemAccessor{}, nil +} + +// The path represent actual literal data so we parse it as a single +// component (It can not contain delegates for this accessor). +func (self DataFilesystemAccessor) ParsePath( + path string) (*accessors.OSPath, error) { + return accessors.MustNewPathspecOSPath("").Clear().Append(path), nil +} + +func (self DataFilesystemAccessor) Lstat( + filename string) (accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + return &accessors.VirtualFileInfo{ + RawData: []byte(filename), + Path: full_path, + }, nil +} + +func (self DataFilesystemAccessor) LstatWithOSPath( + full_path *accessors.OSPath) (accessors.FileInfo, error) { + return &accessors.VirtualFileInfo{ + RawData: []byte(full_path.String()), + Path: full_path, + }, nil +} + +func (self DataFilesystemAccessor) ReadDir( + path string) ([]accessors.FileInfo, error) { + return nil, errors.New("Not implemented") +} + +func (self DataFilesystemAccessor) ReadDirWithOSPath( + path *accessors.OSPath) ([]accessors.FileInfo, error) { + return nil, errors.New("Not implemented") +} + +func (self DataFilesystemAccessor) Open( + path string) (accessors.ReadSeekCloser, error) { + return accessors.VirtualReadSeekCloser{ + ReadSeeker: strings.NewReader(path), + }, nil +} + +func (self DataFilesystemAccessor) OpenWithOSPath( + path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + return accessors.VirtualReadSeekCloser{ + ReadSeeker: strings.NewReader(path.String()), + }, nil +} + +func init() { + accessors.Register(&DataFilesystemAccessor{}) +} diff --git a/accessors/data/data_test.go b/accessors/data/data_test.go new file mode 100644 index 000000000..ce2dce091 --- /dev/null +++ b/accessors/data/data_test.go @@ -0,0 +1,41 @@ +package data + +import ( + "io/ioutil" + "testing" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vtesting/assert" +) + +func TestAccessorData(t *testing.T) { + scope := vql_subsystem.MakeScope() + accessor, err := accessors.GetAccessor("data", scope) + assert.NoError(t, err) + + fd, err := accessor.Open("Hello world") + assert.NoError(t, err) + + data, err := ioutil.ReadAll(fd) + assert.NoError(t, err) + + assert.Equal(t, "Hello world", string(data)) +} + +func TestAccessorScope(t *testing.T) { + scope := vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set("Foobar", "Hello world")) + + accessor, err := accessors.GetAccessor("scope", scope) + assert.NoError(t, err) + + fd, err := accessor.Open("Foobar") + assert.NoError(t, err) + + data, err := ioutil.ReadAll(fd) + assert.NoError(t, err) + + assert.Equal(t, "Hello world", string(data)) +} diff --git a/accessors/data/scope.go b/accessors/data/scope.go new file mode 100644 index 000000000..039020e30 --- /dev/null +++ b/accessors/data/scope.go @@ -0,0 +1,122 @@ +package data + +import ( + "context" + "fmt" + "strings" + + "github.com/go-errors/errors" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/types" +) + +type ScopeFilesystemAccessor struct { + scope vfilter.Scope +} + +func (self ScopeFilesystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "scope", + Description: `Present the content of a scope variable as a file.`, + } +} + +func (self ScopeFilesystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + return ScopeFilesystemAccessor{scope}, nil +} + +func (self ScopeFilesystemAccessor) getData(variable string) (string, error) { + var result vfilter.Any = self.scope + var pres bool + + for _, member := range strings.Split(variable, ".") { + switch t := result.(type) { + case types.LazyExpr: + result = t.Reduce(context.Background()) + } + result, pres = self.scope.Associative(result, member) + if !pres { + return "", utils.NotFoundError + } + } + + switch t := result.(type) { + case string: + return t, nil + + case []byte: + return string(t), nil + + default: + return fmt.Sprintf("%v", result), nil + } +} + +func (self ScopeFilesystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.MustNewPathspecOSPath("").Clear().Append(path), nil +} + +func (self ScopeFilesystemAccessor) LstatWithOSPath(path *accessors.OSPath) ( + accessors.FileInfo, error) { + if len(path.Components) != 1 { + return nil, utils.NotFoundError + } + + return self.Lstat(path.Components[0]) +} + +func (self ScopeFilesystemAccessor) Lstat(variable string) ( + accessors.FileInfo, error) { + str, err := self.getData(variable) + if err != nil { + return nil, err + } + + full_path, err := self.ParsePath(variable) + if err != nil { + return nil, err + } + + return &accessors.VirtualFileInfo{ + RawData: []byte(str), + Path: full_path, + }, nil +} + +func (self ScopeFilesystemAccessor) ReadDir(path string) ( + []accessors.FileInfo, error) { + return nil, errors.New("Not implemented") +} + +func (self ScopeFilesystemAccessor) ReadDirWithOSPath(path *accessors.OSPath) ( + []accessors.FileInfo, error) { + return nil, errors.New("Not implemented") +} + +func (self ScopeFilesystemAccessor) OpenWithOSPath(path *accessors.OSPath) ( + accessors.ReadSeekCloser, error) { + if len(path.Components) != 1 { + return nil, utils.NotFoundError + } + + return self.Open(path.Components[0]) +} + +func (self ScopeFilesystemAccessor) Open(path string) ( + accessors.ReadSeekCloser, error) { + str, err := self.getData(path) + if err != nil { + return nil, err + } + return accessors.VirtualReadSeekCloser{ + ReadSeeker: strings.NewReader(str), + }, nil +} + +func init() { + accessors.Register(&ScopeFilesystemAccessor{}) +} diff --git a/accessors/deny.go b/accessors/deny.go new file mode 100644 index 000000000..1f92b1d14 --- /dev/null +++ b/accessors/deny.go @@ -0,0 +1,92 @@ +package accessors + +import ( + "fmt" + + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +var ( + NotImplementedError = utils.NotImplementedError +) + +type UnimplementedAccessor struct { + Name string +} + +func (self UnimplementedAccessor) ReadDir(path string) ([]FileInfo, error) { + return nil, fmt.Errorf("%v: %w: Accessor denied by configuration", + self.Name, NotImplementedError) +} + +func (self UnimplementedAccessor) Open(path string) (ReadSeekCloser, error) { + return nil, fmt.Errorf("%v: %w: Accessor denied by configuration", + self.Name, NotImplementedError) +} + +func (self UnimplementedAccessor) Lstat(filename string) (FileInfo, error) { + return nil, fmt.Errorf("%v: %w: Accessor denied by configuration", + self.Name, NotImplementedError) +} + +func (self UnimplementedAccessor) ParsePath(filename string) (*OSPath, error) { + return nil, fmt.Errorf("%v: %w: Accessor denied by configuration", + self.Name, NotImplementedError) +} + +func (self UnimplementedAccessor) ReadDirWithOSPath(path *OSPath) ([]FileInfo, error) { + return nil, fmt.Errorf("%v: %w: Accessor denied by configuration", + self.Name, NotImplementedError) +} + +func (self UnimplementedAccessor) OpenWithOSPath(path *OSPath) (ReadSeekCloser, error) { + return nil, fmt.Errorf("%v: %w: Accessor denied by configuration", + self.Name, NotImplementedError) +} + +func (self UnimplementedAccessor) LstatWithOSPath(path *OSPath) (FileInfo, error) { + return nil, fmt.Errorf("%v: %w: Accessor denied by configuration", + self.Name, NotImplementedError) +} + +func (self UnimplementedAccessor) New(scope vfilter.Scope) (FileSystemAccessor, error) { + return nil, fmt.Errorf("%v: %w: Accessor denied by configuration", + self.Name, NotImplementedError) +} + +func (self UnimplementedAccessor) Describe() *AccessorDescriptor { + return &AccessorDescriptor{ + Name: self.Name, + Description: "Blocked accessor", + } +} + +func EnforceAccessorAllowList( + allowed_accessors []string, deny_accessors []string) error { + mu.Lock() + defer mu.Unlock() + + global_manager := globalDeviceManager + + if len(allowed_accessors) > 0 { + globalDeviceManager = NewDefaultDeviceManager() + + for _, allowed := range allowed_accessors { + impl, ok := global_manager.handlers[allowed] + if !ok { + return fmt.Errorf("Unknown accessor in allow list: %v", allowed) + } + + globalDeviceManager.handlers[allowed] = impl + } + } + + for _, deny := range deny_accessors { + globalDeviceManager.Register(&UnimplementedAccessor{ + Name: deny, + }) + } + + return nil +} diff --git a/accessors/ewf/cache.go b/accessors/ewf/cache.go new file mode 100644 index 000000000..37bd94116 --- /dev/null +++ b/accessors/ewf/cache.go @@ -0,0 +1,168 @@ +package ewf + +import ( + "errors" + "io" + "strings" + "sync" + + "github.com/Velocidex/go-ewf/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +const ( + EWF_CACHE_TAG = "__EWF_CACHE" +) + +// Don't bother expiring this until the end of the query. +type ewfCache struct { + mu sync.Mutex + + cache map[string]*EWFReader +} + +func (self *ewfCache) Get(key string) (*EWFReader, bool) { + self.mu.Lock() + defer self.mu.Unlock() + + r, pres := self.cache[key] + return r, pres +} + +func (self *ewfCache) Set(key string, r *EWFReader) { + self.mu.Lock() + defer self.mu.Unlock() + + self.cache[key] = r +} + +func (self *ewfCache) Close() { + self.mu.Lock() + defer self.mu.Unlock() + + for _, r := range self.cache { + r._ReallyClose() + } +} + +func getCachedEWFFile( + full_path *accessors.OSPath, + accessor accessors.FileSystemAccessor, + scope vfilter.Scope) (*EWFReader, error) { + + cache, pres := vql_subsystem.CacheGet(scope, EWF_CACHE_TAG).(*ewfCache) + if !pres { + cache = &ewfCache{ + cache: make(map[string]*EWFReader), + } + err := vql_subsystem.GetRootScope(scope).AddDestructor(cache.Close) + if err != nil { + return nil, err + } + vql_subsystem.CacheSet(scope, EWF_CACHE_TAG, cache) + } + + key := full_path.String() + res, pres := cache.Get(key) + if pres { + // Give a copy of the cache object so it can be seeked + // independently. + return res.Copy(), nil + } + + // Try to open the EWF file + options := &parser.EWFOptions{ + LRUSize: 100, + } + + files, err := getAllVolumes(full_path, accessor, scope) + if err != nil { + return nil, err + } + + if len(files) == 0 { + return nil, errors.New("No volumes found") + } + + // Adapt all these readers for the EWF object + files_readat := make([]io.ReaderAt, 0, len(files)) + for _, r := range files { + files_readat = append(files_readat, utils.MakeReaderAtter(r)) + } + + ewf_volume, err := parser.OpenEWFFile(options, files_readat...) + if err != nil { + for _, fd := range files { + fd.Close() + } + return nil, err + } + + ewf := &EWFReader{ + readers: files, + ewf: ewf_volume, + } + + cache.Set(key, ewf) + scope.Log("ewf: Opened EWF file %v\n", key) + + return ewf, nil +} + +func getAllVolumes( + full_path *accessors.OSPath, + accessor accessors.FileSystemAccessor, + scope vfilter.Scope) ( + []io.ReadSeekCloser, error) { + + result := []io.ReadSeekCloser{} + + delegate, err := full_path.Delegate(scope) + if err != nil { + return nil, err + } + + basename := delegate.Basename() + dirname := delegate.Dirname() + + if strings.HasSuffix(basename, ".E01") || + strings.HasSuffix(basename, ".e01") { + prefix := basename[:len(basename)-4] + + children, err := accessor.ReadDirWithOSPath(dirname) + if err == nil { + // Technically a volume set can use all the letters so we + // cant assume it has to have an .Exx extension. + for _, c := range children { + if !strings.HasPrefix(c.Name(), prefix) { + continue + } + + extension := c.Name()[len(prefix):] + if len(extension) != 4 || extension[0] != '.' { + continue + } + + fd, err := accessor.OpenWithOSPath(c.OSPath()) + if err == nil { + result = append(result, fd) + } + scope.Log("ewf: Found Segment file %v\n", c.OSPath()) + } + } + } + + if len(result) == 0 { + fd, err := accessor.OpenWithOSPath(delegate) + if err != nil { + return nil, err + } + scope.Log("ewf: Found Segment file %v\n", delegate) + result = append(result, fd) + } + + return result, nil +} diff --git a/accessors/ewf/ewf.go b/accessors/ewf/ewf.go new file mode 100644 index 000000000..2896f9bdb --- /dev/null +++ b/accessors/ewf/ewf.go @@ -0,0 +1,104 @@ +package ewf + +// An accessor that opens an EWF image +import ( + "errors" + "io" + "os" + + "github.com/Velocidex/go-ewf/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/zip" + "www.velocidex.com/golang/vfilter" +) + +type EWFReader struct { + readers []io.ReadSeekCloser + + offset int64 + ewf *parser.EWFFile +} + +func (self *EWFReader) Copy() *EWFReader { + return &EWFReader{ + readers: self.readers, + ewf: self.ewf, + } +} + +// Files will only be closed when the scope is destroyed. This is ok +// because we do not normally have too many EWF files open in the same +// query. +func (self *EWFReader) _ReallyClose() { + for _, r := range self.readers { + r.Close() + } +} + +func (self *EWFReader) Close() error { + return nil +} + +func (self *EWFReader) Read(buff []byte) (int, error) { + n, err := self.ewf.ReadAt(buff, self.offset) + if err != nil { + return 0, err + } + + if n == 0 { + return 0, io.EOF + } + + self.offset += int64(n) + return n, err +} + +func (self *EWFReader) Seek(offset int64, whence int) (int64, error) { + if whence == os.SEEK_SET { + self.offset = offset + } else if whence == os.SEEK_CUR { + self.offset += offset + } + return self.offset, nil +} + +func (self *EWFReader) LStat() (accessors.FileInfo, error) { + return nil, errors.New("Not implemented") +} + +func GetEWFImage(full_path *accessors.OSPath, scope vfilter.Scope) ( + zip.ReaderStat, error) { + + pathspec := full_path.PathSpec() + + // The EWF accessor must use a delegate but if one is not + // provided we use the "auto" accessor, to open the underlying + // file. + if pathspec.DelegateAccessor == "" && pathspec.GetDelegatePath() == "" { + pathspec.DelegatePath = pathspec.Path + pathspec.DelegateAccessor = "auto" + pathspec.Path = "/" + err := full_path.SetPathSpec(pathspec) + if err != nil { + return nil, err + } + } + + accessor, err := accessors.GetAccessor(pathspec.DelegateAccessor, scope) + if err != nil { + scope.Log("ewf: %v: did you provide a DelegateAccessor PathSpec?", err) + return nil, err + } + + return getCachedEWFFile(full_path, accessor, scope) +} + +func init() { + accessors.Register(accessors.DescribeAccessor( + zip.NewGzipFileSystemAccessor( + accessors.MustNewLinuxOSPath(""), GetEWFImage), + accessors.AccessorDescriptor{ + Name: "ewf", + Description: `Allow reading an EWF file.`, + })) +} diff --git a/accessors/ext4/ext4_accessor.go b/accessors/ext4/ext4_accessor.go new file mode 100644 index 000000000..e4895f8c7 --- /dev/null +++ b/accessors/ext4/ext4_accessor.go @@ -0,0 +1,326 @@ +package ext4 + +// This is an accessor which parses a Ext4 filesystem +import ( + "errors" + "fmt" + "io" + "io/fs" + "runtime/debug" + "sync" + + ext4 "github.com/Velocidex/go-ext4/parser" + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/vfilter" +) + +type Ext4FileInfo struct { + *ext4.FileInfo + _full_path *accessors.OSPath +} + +func (self *Ext4FileInfo) IsDir() bool { + return self.Mode().IsDir() +} + +func (self *Ext4FileInfo) Data() *ordereddict.Dict { + data := ordereddict.NewDict(). + Set("Inode", self.Inode()). + Set("Uid", self.Uid()). + Set("Gid", self.Gid()) + + flags := self.Flags() + if len(flags) > 0 { + data.Set("Flags", flags) + } + + return data +} + +func (self *Ext4FileInfo) UniqueName() string { + return self._full_path.String() +} + +func (self *Ext4FileInfo) FullPath() string { + return self._full_path.String() +} + +func (self *Ext4FileInfo) OSPath() *accessors.OSPath { + return self._full_path +} + +// Not supported +func (self *Ext4FileInfo) IsLink() bool { + return self.Mode()&fs.ModeSymlink > 0 +} + +func (self *Ext4FileInfo) GetLink() (*accessors.OSPath, error) { + return nil, errors.New("Not implemented") +} + +type Ext4FileSystemAccessor struct { + scope vfilter.Scope + + // The delegate accessor we use to open the underlying volume. + accessor string + device *accessors.OSPath + + root *accessors.OSPath +} + +func NewExt4FileSystemAccessor( + scope vfilter.Scope, + root_path *accessors.OSPath, + device *accessors.OSPath, accessor string) *Ext4FileSystemAccessor { + return &Ext4FileSystemAccessor{ + scope: scope, + accessor: accessor, + device: device, + root: root_path, + } +} + +func (self Ext4FileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "raw_ext4", + Description: `Access the Ext4 filesystem inside an image by parsing the image.`, + } +} + +func (self Ext4FileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + // Create a new cache in the scope. + return &Ext4FileSystemAccessor{ + scope: scope, + device: self.device, + accessor: self.accessor, + root: self.root, + }, nil +} + +func (self Ext4FileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewLinuxOSPath(path) +} + +func (self *Ext4FileSystemAccessor) ReadDir(path string) ( + res []accessors.FileInfo, err error) { + // Normalize the path + fullpath, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(fullpath) +} + +func (self *Ext4FileSystemAccessor) ReadDirWithOSPath( + fullpath *accessors.OSPath) (res []accessors.FileInfo, err error) { + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + result := []accessors.FileInfo{} + + ext4_ctx, err := GetExt4Context(self.scope, self.device, fullpath, self.accessor) + if err != nil { + return nil, err + } + + // Open the device path from the root. + inode, err := ext4_ctx.OpenInodeWithPath(fullpath.Components) + if err != nil { + return nil, err + } + + dir, err := inode.Dir(ext4_ctx) + if err != nil { + return nil, err + } + + // List the directory. + for _, info := range dir { + name := info.Name() + + // Skip these useless directories. + if name == "" || name == "." || name == ".." { + continue + } + + result = append(result, &Ext4FileInfo{ + FileInfo: info, + _full_path: fullpath.Append(info.Name()), + }) + } + return result, nil +} + +// Adapt a ReadSeeker onto the ReadAtter that go-ntfs provides. +type readAdapter struct { + sync.Mutex + + info accessors.FileInfo + pos int64 + reader io.ReaderAt +} + +func (self *readAdapter) Read(buf []byte) (res int, err error) { + self.Lock() + defer self.Unlock() + + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + res, err = self.reader.ReadAt(buf, self.pos) + + // If ReadAt is unable to read anything it means an EOF. + if res == 0 { + // The NTFS cache may be flushed during this read and in this + // case the file handle will be closed on us during the + // read. This usually shows up as an EOF read with 0 length. + // See Issue + // https://github.com/Velocidex/velociraptor/issues/2153 + + // We catch this issue by issuing one more read just to make + // sure. Usually we are wrapping a ReadAtter here and we do + // not expect to see a EOF anyway. In the case of NTFS the + // extra read will re-open the underlying device file with a + // new NTFS context (reparsing the $MFT and purging all the + // caches) so the next read will succeed. + res, err = self.reader.ReadAt(buf, self.pos) + if res == 0 { + // Still EOF - give up + return res, io.EOF + } + } + + self.pos += int64(res) + + return res, err +} + +func (self *readAdapter) ReadAt(buf []byte, offset int64) (int, error) { + self.Lock() + defer self.Unlock() + self.pos = offset + + return self.reader.ReadAt(buf, offset) +} + +func (self *readAdapter) Close() error { + return nil +} + +func (self *readAdapter) Seek(offset int64, whence int) (int64, error) { + self.Lock() + defer self.Unlock() + + self.pos = offset + return self.pos, nil +} + +func (self *Ext4FileSystemAccessor) Open( + path string) (res accessors.ReadSeekCloser, err error) { + + full_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self *Ext4FileSystemAccessor) OpenWithOSPath( + fullpath *accessors.OSPath) (res accessors.ReadSeekCloser, err error) { + + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + ext4_ctx, err := GetExt4Context(self.scope, self.device, fullpath, self.accessor) + if err != nil { + return nil, err + } + + // Open the device path from the root. + inode, err := ext4_ctx.OpenInodeWithPath(fullpath.Components) + if err != nil { + return nil, err + } + + stream, err := inode.GetReader(ext4_ctx) + if err != nil { + return nil, err + } + + return &readAdapter{ + info: &Ext4FileInfo{ + FileInfo: inode.Stat(), + _full_path: fullpath, + }, + reader: stream, + }, nil +} + +func (self *Ext4FileSystemAccessor) Lstat( + path string) (res accessors.FileInfo, err error) { + + fullpath, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(fullpath) +} + +func (self *Ext4FileSystemAccessor) LstatWithOSPath( + fullpath *accessors.OSPath) (res accessors.FileInfo, err error) { + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + ext4_ctx, err := GetExt4Context(self.scope, self.device, fullpath, self.accessor) + if err != nil { + return nil, err + } + + inode, err := ext4_ctx.OpenInodeWithPath(fullpath.Components) + if err != nil { + return nil, err + } + + stat := inode.Stat() + return &Ext4FileInfo{ + FileInfo: stat, + _full_path: fullpath, + }, nil +} + +func init() { + accessors.Register(&Ext4FileSystemAccessor{}) + + json.RegisterCustomEncoder(&Ext4FileInfo{}, accessors.MarshalGlobFileInfo) +} diff --git a/accessors/ext4/ext4_accessor_linux.go b/accessors/ext4/ext4_accessor_linux.go new file mode 100644 index 000000000..a979bf5da --- /dev/null +++ b/accessors/ext4/ext4_accessor_linux.go @@ -0,0 +1,102 @@ +//go:build linux +// +build linux + +/* + This accessor is similar to the Windows ntfs accessor. It + automatically enumerates the mount points and attaches a raw ext4 + mount to each mounted device. + + Users can use the same path as is presented on the real system, but + the raw ext4 partitions will be parsed instead. + + This accessor is only available under linux. +*/ + +package ext4 + +import ( + "context" + + "www.velocidex.com/golang/velociraptor/accessors" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/psutils" + "www.velocidex.com/golang/vfilter" +) + +const ( + EXT4_Tag = "_EXT4_Tag" +) + +type LinuxExt4FileSystemAccessor struct { + *accessors.MountFileSystemAccessor +} + +func (self LinuxExt4FileSystemAccessor) GetVirtualFS(scope vfilter.Scope) ( + *accessors.MountFileSystemAccessor, error) { + + mount_fs, ok := vql_subsystem.CacheGet(scope, EXT4_Tag).(*accessors.MountFileSystemAccessor) + if ok { + return mount_fs, nil + } + + root_path := accessors.MustNewLinuxOSPath("/") + virtual_fs := accessors.NewVirtualFilesystemAccessor(root_path) + mount_fs = accessors.NewMountFileSystemAccessor(root_path, virtual_fs) + + vql_subsystem.CacheSet(scope, EXT4_Tag, mount_fs) + + ctx := context.Background() + partitions, err := psutils.PartitionsWithContext(ctx) + if err != nil { + return nil, err + } + + for _, p := range partitions { + if p.Fstype != "ext4" { + continue + } + + // Mount the partition + target, err := accessors.NewLinuxOSPath(p.Mountpoint) + if err != nil { + continue + } + + device, err := accessors.NewLinuxOSPath(p.Device) + if err != nil { + continue + } + + scope.Log("ext4: Adding mapping to %v on %v", + device, p.Mountpoint) + + // Need to use raw_file to be able to open a device file. + mount_fs.AddMapping(root_path, + target, + NewExt4FileSystemAccessor( + scope, root_path, device, "raw_file")) + } + + return mount_fs, nil +} + +func (self LinuxExt4FileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + mount_fs, err := self.GetVirtualFS(scope) + // Create a new cache in the scope. + return &LinuxExt4FileSystemAccessor{ + MountFileSystemAccessor: mount_fs, + }, err +} + +func (self LinuxExt4FileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "ext4", + Description: `Access files by parsing the raw ext4 filesystems.`, + } +} + +func init() { + accessors.Register(&LinuxExt4FileSystemAccessor{}) +} diff --git a/accessors/ext4/utils.go b/accessors/ext4/utils.go new file mode 100644 index 000000000..b03939766 --- /dev/null +++ b/accessors/ext4/utils.go @@ -0,0 +1,59 @@ +package ext4 + +import ( + ext4 "github.com/Velocidex/go-ext4/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/constants" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/readers" + "www.velocidex.com/golang/vfilter" +) + +func GetExt4Context(scope vfilter.Scope, + device, fullpath *accessors.OSPath, accessor string) ( + result *ext4.EXT4Context, err error) { + + if device == nil { + device, err = fullpath.Delegate(scope) + if err != nil { + return nil, err + } + accessor = fullpath.DelegateAccessor() + } + + return GetExt4Cache(scope, device, accessor) +} + +func GetExt4Cache(scope vfilter.Scope, + device *accessors.OSPath, accessor string) (*ext4.EXT4Context, error) { + key := "ext4_cache" + device.String() + accessor + + // Get the cache context from the root scope's cache + cache_ctx, ok := vql_subsystem.CacheGet(scope, key).(*ext4.EXT4Context) + if !ok { + lru_size := vql_subsystem.GetIntFromRow( + scope, scope, constants.NTFS_CACHE_SIZE) + + paged_reader, err := readers.NewAccessorReader( + scope, accessor, device, int(lru_size)) + if err != nil { + return nil, err + } + + cache_ctx, err = ext4.GetEXT4Context(paged_reader) + if err != nil { + return nil, err + } + vql_subsystem.CacheSet(scope, key, cache_ctx) + + // Close the device when we are done with this query. + err = vql_subsystem.GetRootScope(scope).AddDestructor(func() { + paged_reader.Close() + }) + if err != nil { + return nil, err + } + } + + return cache_ctx, nil +} diff --git a/accessors/fat/fat_accessor.go b/accessors/fat/fat_accessor.go new file mode 100644 index 000000000..010905eca --- /dev/null +++ b/accessors/fat/fat_accessor.go @@ -0,0 +1,342 @@ +package fat + +// This is an accessor which parses a FAT filesystem +import ( + "errors" + "fmt" + "io" + "os" + "runtime/debug" + "sync" + "time" + + fat "github.com/Velocidex/go-fat/parser" + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/vfilter" +) + +const ( + // Scope cache tag for the FAT parser + FATFileSystemTag = "_FAT" +) + +type FATFileInfo struct { + info *fat.DirectoryEntry + _full_path *accessors.OSPath +} + +func (self *FATFileInfo) IsDir() bool { + return self.info.IsDir +} + +func (self *FATFileInfo) Size() int64 { + return int64(self.info.Size) +} + +func (self *FATFileInfo) Data() *ordereddict.Dict { + result := ordereddict.NewDict(). + Set("first_cluster", self.info.FirstCluster). + Set("attr", self.info.Attribute). + Set("short_name", self.info.ShortName) + + if self.info.IsDeleted { + result.Set("deleted", true) + } + + return result +} + +func (self *FATFileInfo) Name() string { + return self.info.Name +} + +func (self *FATFileInfo) UniqueName() string { + return self._full_path.String() +} + +func (self *FATFileInfo) Mode() os.FileMode { + var result os.FileMode = 0755 + if self.IsDir() { + result |= os.ModeDir + } + return result +} + +func (self *FATFileInfo) ModTime() time.Time { + return self.info.Mtime +} + +func (self *FATFileInfo) FullPath() string { + return self._full_path.String() +} + +func (self *FATFileInfo) OSPath() *accessors.OSPath { + return self._full_path +} + +func (self *FATFileInfo) Btime() time.Time { + return self.info.Ctime +} + +func (self *FATFileInfo) Mtime() time.Time { + return self.info.Mtime +} + +func (self *FATFileInfo) Ctime() time.Time { + return self.info.Ctime +} + +func (self *FATFileInfo) Atime() time.Time { + return self.info.Atime +} + +// Not supported +func (self *FATFileInfo) IsLink() bool { + return false +} + +func (self *FATFileInfo) GetLink() (*accessors.OSPath, error) { + return nil, errors.New("Not implemented") +} + +type FATFileSystemAccessor struct { + scope vfilter.Scope + + // The delegate accessor we use to open the underlying volume. + accessor string + device *accessors.OSPath + + root *accessors.OSPath +} + +func (self FATFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "fat", + Description: `Access the FAT filesystem inside an image by parsing FAT.`, + } +} + +func (self FATFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + // Create a new cache in the scope. + return &FATFileSystemAccessor{ + scope: scope, + device: self.device, + accessor: self.accessor, + root: self.root, + }, nil +} + +func (self FATFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewWindowsNTFSPath(path) +} + +func (self *FATFileSystemAccessor) ReadDir(path string) ( + res []accessors.FileInfo, err error) { + // Normalize the path + fullpath, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(fullpath) +} + +func (self *FATFileSystemAccessor) ReadDirWithOSPath( + fullpath *accessors.OSPath) (res []accessors.FileInfo, err error) { + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + result := []accessors.FileInfo{} + + fat_ctx, err := GetFatContext(self.scope, self.device, fullpath, self.accessor) + if err != nil { + return nil, err + } + + // Open the device path from the root. + dir, err := fat_ctx.ListDirectoryComponents(fullpath.Components) + if err != nil { + return nil, err + } + + // List the directory. + for _, info := range dir { + // Skip these useless directories. + if info.Name == "." || info.Name == ".." { + continue + } + + result = append(result, &FATFileInfo{ + info: info, + _full_path: fullpath.Append(info.Name), + }) + } + return result, nil +} + +// Adapt a ReadSeeker onto the ReadAtter that go-ntfs provides. +type readAdapter struct { + sync.Mutex + + info accessors.FileInfo + pos int64 + reader io.ReaderAt +} + +func (self *readAdapter) Read(buf []byte) (res int, err error) { + self.Lock() + defer self.Unlock() + + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + res, err = self.reader.ReadAt(buf, self.pos) + + // If ReadAt is unable to read anything it means an EOF. + if res == 0 { + // The NTFS cache may be flushed during this read and in this + // case the file handle will be closed on us during the + // read. This usually shows up as an EOF read with 0 length. + // See Issue + // https://github.com/Velocidex/velociraptor/issues/2153 + + // We catch this issue by issuing one more read just to make + // sure. Usually we are wrapping a ReadAtter here and we do + // not expect to see a EOF anyway. In the case of NTFS the + // extra read will re-open the underlying device file with a + // new NTFS context (reparsing the $MFT and purging all the + // caches) so the next read will succeed. + res, err = self.reader.ReadAt(buf, self.pos) + if res == 0 { + // Still EOF - give up + return res, io.EOF + } + } + + self.pos += int64(res) + + return res, err +} + +func (self *readAdapter) ReadAt(buf []byte, offset int64) (int, error) { + self.Lock() + defer self.Unlock() + self.pos = offset + + return self.reader.ReadAt(buf, offset) +} + +func (self *readAdapter) Close() error { + return nil +} + +func (self *readAdapter) Seek(offset int64, whence int) (int64, error) { + self.Lock() + defer self.Unlock() + + self.pos = offset + return self.pos, nil +} + +func (self *FATFileSystemAccessor) Open( + path string) (res accessors.ReadSeekCloser, err error) { + + full_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self *FATFileSystemAccessor) OpenWithOSPath( + fullpath *accessors.OSPath) (res accessors.ReadSeekCloser, err error) { + + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + fat_ctx, err := GetFatContext(self.scope, self.device, fullpath, self.accessor) + if err != nil { + return nil, err + } + + // Open the device path from the root. + stream, err := fat_ctx.OpenComponents(fullpath.Components) + if err != nil { + return nil, err + } + + return &readAdapter{ + info: &FATFileInfo{ + info: stream.Info, + _full_path: fullpath, + }, + reader: stream, + }, nil +} + +func (self *FATFileSystemAccessor) Lstat( + path string) (res accessors.FileInfo, err error) { + + fullpath, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(fullpath) +} + +func (self *FATFileSystemAccessor) LstatWithOSPath( + fullpath *accessors.OSPath) (res accessors.FileInfo, err error) { + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + fat_ctx, err := GetFatContext(self.scope, self.device, fullpath, self.accessor) + if err != nil { + return nil, err + } + + stat, err := fat_ctx.StatComponents(fullpath.Components) + if err != nil { + return nil, err + } + + return &FATFileInfo{ + info: stat, + _full_path: fullpath, + }, nil +} + +func init() { + accessors.Register(&FATFileSystemAccessor{}) + + json.RegisterCustomEncoder(&FATFileInfo{}, accessors.MarshalGlobFileInfo) +} diff --git a/accessors/fat/utils.go b/accessors/fat/utils.go new file mode 100644 index 000000000..026ac748b --- /dev/null +++ b/accessors/fat/utils.go @@ -0,0 +1,59 @@ +package fat + +import ( + fat "github.com/Velocidex/go-fat/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/constants" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/readers" + "www.velocidex.com/golang/vfilter" +) + +func GetFatContext(scope vfilter.Scope, + device, fullpath *accessors.OSPath, accessor string) ( + result *fat.FATContext, err error) { + + if device == nil { + device, err = fullpath.Delegate(scope) + if err != nil { + return nil, err + } + accessor = fullpath.DelegateAccessor() + } + + return GetFATCache(scope, device, accessor) +} + +func GetFATCache(scope vfilter.Scope, + device *accessors.OSPath, accessor string) (*fat.FATContext, error) { + key := "fat_cache" + device.String() + accessor + + // Get the cache context from the root scope's cache + cache_ctx, ok := vql_subsystem.CacheGet(scope, key).(*fat.FATContext) + if !ok { + lru_size := vql_subsystem.GetIntFromRow( + scope, scope, constants.NTFS_CACHE_SIZE) + + paged_reader, err := readers.NewAccessorReader( + scope, accessor, device, int(lru_size)) + if err != nil { + return nil, err + } + + cache_ctx, err = fat.GetFATContext(paged_reader) + if err != nil { + return nil, err + } + vql_subsystem.CacheSet(scope, key, cache_ctx) + + // Close the device when we are done with this query. + err = vql_subsystem.GetRootScope(scope).AddDestructor(func() { + paged_reader.Close() + }) + if err != nil { + return nil, err + } + } + + return cache_ctx, nil +} diff --git a/accessors/file/accessor_common.go b/accessors/file/accessor_common.go new file mode 100644 index 000000000..71d44dc37 --- /dev/null +++ b/accessors/file/accessor_common.go @@ -0,0 +1,540 @@ +//go:build linux || darwin || freebsd +// +build linux darwin freebsd + +package file + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "syscall" + "time" + + "github.com/Velocidex/ordereddict" + errors "github.com/go-errors/errors" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +var ( + fileAccessorCurrentOpened = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "accessor_file_current_open", + Help: "Number of currently opened files with the file accessor.", + }) + + ErrNotFound = errors.New("file not found") +) + +type _inode struct { + dev, inode uint64 +} + +// Keep track of symlinks we visited. +type AccessorContext struct { + mu sync.Mutex + + links map[_inode]bool +} + +func (self *AccessorContext) LinkVisited(dev, inode uint64) { + id := _inode{dev, inode} + + self.mu.Lock() + defer self.mu.Unlock() + + self.links[id] = true +} + +func (self *AccessorContext) WasLinkVisited(dev, inode uint64) bool { + id := _inode{dev, inode} + + self.mu.Lock() + defer self.mu.Unlock() + + _, pres := self.links[id] + return pres +} + +type OSFileInfo struct { + _FileInfo os.FileInfo + _full_path *accessors.OSPath + _accessor_ctx *AccessorContext + _fstype string +} + +func NewOSFileInfo(base os.FileInfo, path *accessors.OSPath) *OSFileInfo { + return &OSFileInfo{ + _FileInfo: base, + _full_path: path, + _accessor_ctx: &AccessorContext{ + links: make(map[_inode]bool), + }, + } +} + +func (self *OSFileInfo) OSPath() *accessors.OSPath { + return self._full_path +} + +func (self *OSFileInfo) Size() int64 { + return self._FileInfo.Size() +} + +func (self *OSFileInfo) Name() string { + return self._FileInfo.Name() +} + +func (self *OSFileInfo) IsDir() bool { + return self._FileInfo.IsDir() +} + +func (self *OSFileInfo) ModTime() time.Time { + return self._FileInfo.ModTime() +} + +func (self *OSFileInfo) Mode() os.FileMode { + return self._FileInfo.Mode() +} + +func (self *OSFileInfo) Sys() interface{} { + return self._FileInfo.Sys() +} + +func (self *OSFileInfo) Dev() uint64 { + sys, ok := self._FileInfo.Sys().(*syscall.Stat_t) + if !ok { + return 0 + } + return uint64(sys.Dev) +} + +func (self *OSFileInfo) Data() *ordereddict.Dict { + result := ordereddict.NewDict() + if self.IsLink() { + path := self.FullPath() + target, err := os.Readlink(path) + if err == nil { + result.Set("Link", target) + } + } + + sys, ok := self._FileInfo.Sys().(*syscall.Stat_t) + if ok { + major, minor := splitDevNumber(uint64(sys.Dev)) + result.Set("DevMajor", major). + Set("DevMinor", minor) + } + + if self._fstype != "" { + result.Set("FSType", self._fstype) + } + + return result +} + +func (self *OSFileInfo) FullPath() string { + return self._full_path.String() +} + +func (self *OSFileInfo) IsLink() bool { + return self.Mode()&os.ModeSymlink != 0 +} + +func (self *OSFileInfo) GetLink() (*accessors.OSPath, error) { + sys, ok := self._FileInfo.Sys().(*syscall.Stat_t) + if !ok { + return nil, errors.New("Symlink not supported") + } + + if self._accessor_ctx.WasLinkVisited(uint64(sys.Dev), sys.Ino) { + return nil, errors.New("Symlink cycle detected") + } + self._accessor_ctx.LinkVisited(uint64(sys.Dev), sys.Ino) + + // For now we dont support links so we dont get stuck in a + // cycle. + ret, err := os.Readlink(self._full_path.String()) + if err != nil { + return nil, err + } + + return self._full_path.Parse(ret) +} + +func (self *OSFileInfo) _Sys() *syscall.Stat_t { + return self._FileInfo.Sys().(*syscall.Stat_t) +} + +// Real implementation for non windows OSs: +type OSFileSystemAccessor struct { + context *AccessorContext + + nocase bool + + root *accessors.OSPath + + scope vfilter.Scope +} + +func (self OSFileSystemAccessor) ParsePath(path string) (*accessors.OSPath, error) { + return self.root.Parse(path) +} + +func (self OSFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "file", + Description: `Access files using the operating system's API. Does not allow access to raw devices.`, + Permissions: []acls.ACL_PERMISSION{acls.FILESYSTEM_READ}, + } +} + +func (self OSFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + // Check we have permission to open files. + return &OSFileSystemAccessor{ + context: &AccessorContext{ + links: make(map[_inode]bool), + }, + root: self.root, + nocase: self.nocase, + scope: scope, + }, nil +} + +// Get the closest matching filename from the directory +func getNoCase(filename *accessors.OSPath) (*accessors.OSPath, error) { + if len(filename.Components) == 0 { + return nil, ErrNotFound + } + + parent := filename.Dirname() + dirname := parent.PathSpec().Path + basename := filename.Basename() + + names, err := utils.ReadDirNames(dirname) + if err != nil { + // If we are unable to open the current directory, it may be + // that the parent directory casing is not + // correct. Recursively get the correct parent's casing and + // try again. + nocase_parent, err1 := getNoCase(parent) + if err1 != nil { + return nil, err + } + + dirname := nocase_parent.PathSpec().Path + names, err1 = utils.ReadDirNames(dirname) + if err1 != nil { + return nil, err + } + + // Found the correct parent, keep going. + parent = nocase_parent + } + + for _, name := range names { + if strings.EqualFold(name, basename) { + return parent.Append(name), nil + } + } + + return nil, ErrNotFound +} + +func (self OSFileSystemAccessor) Lstat(filename string) (accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +// On Windows filesystems are usually case insensitive. +func (self OSFileSystemAccessor) GetCanonicalFilename( + path *accessors.OSPath) string { + return path.String() +} + +func (self OSFileSystemAccessor) LstatWithOSPath( + full_path *accessors.OSPath) (accessors.FileInfo, error) { + + defer Instrument("LstatWithOSPath")() + + err := CheckPrefix(full_path) + if err != nil { + return nil, err + } + + filename := full_path.PathSpec().Path + + lstat, err := os.Lstat(filename) + if err != nil { + if !self.nocase { + return nil, err + } + + // Try to get a case insensitive match + nocase_name, err1 := getNoCase(full_path) + if err1 != nil { + return nil, err + } + + // Try again with the nocase filename + filename = nocase_name.PathSpec().Path + lstat, err1 = os.Lstat(filename) + if err1 != nil { + return nil, err + } + + // From here on the filename is correct. + } + + return &OSFileInfo{ + _FileInfo: lstat, + _full_path: full_path.Copy(), + _accessor_ctx: self.context, + }, nil +} + +func (self OSFileSystemAccessor) ReadDir(dir string) ([]accessors.FileInfo, error) { + full_path, err := self.root.Parse(dir) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(full_path) +} + +func (self *OSFileSystemAccessor) GetUnderlyingAPIFilename( + full_path *accessors.OSPath) (string, error) { + return full_path.PathSpec().Path, nil +} + +func (self OSFileSystemAccessor) ReadDirWithOSPath( + full_path *accessors.OSPath) ([]accessors.FileInfo, error) { + + defer Instrument("ReadDirWithOSPath")() + + err := CheckPrefix(full_path) + if err != nil { + return nil, err + } + + dir := full_path.PathSpec().Path + + lstat, err := os.Lstat(dir) + if err != nil { + if !self.nocase { + return nil, err + } + + // Try to get a case insensitive match + nocase_name, err1 := getNoCase(full_path) + if err1 != nil { + return nil, err + } + dir = nocase_name.PathSpec().Path + lstat, err1 = os.Lstat(dir) + if err1 != nil { + return nil, err + } + + // From here below dir is the correct path casing. + } + + // Support symlinks and directories. + if lstat.Mode()&os.ModeSymlink == 0 { + // Not a symlink + if !lstat.IsDir() { + return nil, nil + } + } else { + // If it is a symlink, we need to check the target of the + // symlink and make sure it is a directory. + target, err := filepath.EvalSymlinks(dir) + if err == nil { + // The target is interpreted relative to the directory of + // the link. + if !strings.HasPrefix(target, "/") { + target = full_path.Dirname().PathSpec().Path + "/" + target + } + lstat, err := os.Lstat(target) + + // Target of the link is not there or inaccessible or + // points to something that is not a directory - just + // ignore it with no errors. + if err != nil || !lstat.IsDir() { + return nil, nil + } + + sys, ok := lstat.Sys().(*syscall.Stat_t) + if ok { + // Keep track of the links we visited. + if self.context.WasLinkVisited( + uint64(sys.Dev), sys.Ino) { + return nil, errors.New("Symlink cycle detected") + } + self.context.LinkVisited(uint64(sys.Dev), sys.Ino) + } + } + dir = target + } + + dirfstype := getFSType(dir) + + files, err := utils.ReadDir(dir) + if err != nil { + return nil, err + } + + var result []accessors.FileInfo + for _, f := range files { + fp := full_path.Append(f.Name()) + err := CheckPrefix(fp) + if err != nil { + continue + } + + var fstype string + if f.IsDir() { + fstype = getFSType(fp.String()) + } else { + fstype = dirfstype + } + result = append(result, + &OSFileInfo{ + _FileInfo: f, + _full_path: fp, + _accessor_ctx: self.context, + _fstype: fstype, + }) + } + + return result, nil +} + +// Wrap the os.File object to keep track of open file handles. +type OSFileWrapper struct { + *os.File + closed bool +} + +func (self *OSFileWrapper) DebugString() string { + return fmt.Sprintf("OSFileWrapper %v (closed %v)", self.Name(), self.closed) +} + +func (self *OSFileWrapper) Close() error { + fileAccessorCurrentOpened.Dec() + self.closed = true + return self.File.Close() +} + +func (self *OSFileSystemAccessor) Open(path string) (accessors.ReadSeekCloser, error) { + // Clean the path + full_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self OSFileSystemAccessor) OpenWithOSPath( + full_path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + + defer Instrument("OpenWithOSPath")() + + err := CheckPrefix(full_path) + if err != nil { + return nil, err + } + + path := full_path.PathSpec().Path + + // Eval any symlinks directly + symlink_path, err := filepath.EvalSymlinks(path) + if err != nil { + if !self.nocase { + return nil, err + } + + // Try to get a case insensitive match + nocase_name, err1 := getNoCase(full_path) + if err1 != nil { + return nil, err + } + + // Try again with the nocase filename + path = nocase_name.PathSpec().Path + symlink_path, err1 = filepath.EvalSymlinks(path) + if err1 != nil { + return nil, err + } + + // From here on path is correct. + } + + path = symlink_path + + // Usually we dont allow direct access to devices otherwise a + // recursive yara scan can get into /proc/ and crash the + // kernel. Sometimes this is exactly what we want so we provide + // the "raw_file" accessor. + lstat, err := os.Stat(path) + if err != nil { + return nil, err + } + + if !lstat.Mode().IsDir() && + !lstat.Mode().IsRegular() { + return nil, fmt.Errorf( + "Only regular files supported (not %v)", path) + } + + file, err := os.Open(path) + if err != nil { + return nil, err + } + + fileAccessorCurrentOpened.Inc() + return &OSFileWrapper{File: file}, nil +} + +func init() { + root_path, _ := accessors.NewLinuxOSPath("") + accessors.Register(&OSFileSystemAccessor{ + root: root_path, + }) + + accessors.Register(accessors.DescribeAccessor( + &OSFileSystemAccessor{ + root: root_path, + nocase: true, + }, accessors.AccessorDescriptor{ + Name: "file_nocase", + Description: `Access files using the operating system's API. This is case insensitive - even on Unix Operating systems.`, + Permissions: []acls.ACL_PERMISSION{acls.FILESYSTEM_READ}, + })) + + // On Linux the auto accessor is the same as file. + accessors.Register(accessors.DescribeAccessor( + &OSFileSystemAccessor{ + root: root_path, + }, accessors.AccessorDescriptor{ + Name: "auto", + Description: `Access the file using the best accessor possible. On windows we fall back to NTFS parsing in case the file is locked or unreadable.`, + Permissions: []acls.ACL_PERMISSION{acls.FILESYSTEM_READ}, + })) + + json.RegisterCustomEncoder(&OSFileInfo{}, accessors.MarshalGlobFileInfo) +} diff --git a/accessors/file/accessor_darwin.go b/accessors/file/accessor_darwin.go new file mode 100644 index 000000000..1dd1bcb67 --- /dev/null +++ b/accessors/file/accessor_darwin.go @@ -0,0 +1,69 @@ +//go:build darwin +// +build darwin + +/* + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package file + +import ( + "syscall" + "time" +) + +func (self *OSFileInfo) Btime() time.Time { + ts := self._Sys().Birthtimespec + return time.Unix(0, ts.Nsec+ts.Sec*1000000000) +} + +func (self *OSFileInfo) Mtime() time.Time { + ts := self._Sys().Mtimespec + return time.Unix(0, ts.Nsec+ts.Sec*1000000000) +} + +func (self *OSFileInfo) Ctime() time.Time { + ts := self._Sys().Ctimespec + return time.Unix(0, ts.Nsec+ts.Sec*1000000000) +} + +func (self *OSFileInfo) Atime() time.Time { + ts := self._Sys().Atimespec + return time.Unix(0, ts.Nsec+ts.Sec*1000000000) +} + +func splitDevNumber(dev uint64) (major, minor uint64) { + // See xnu/bsd/sys/types.h + major = (dev >> 24) & 0xff + minor = dev & 0xffffff + return +} + +func getFSType(path string) string { + var st syscall.Statfs_t + if err := syscall.Statfs(path, &st); err != nil { + return "" + } + var name []byte + for _, c := range st.Fstypename { + if c == 0 { + break + } + name = append(name, byte(c)) + } + return string(name) +} diff --git a/accessors/file/accessor_freebsd.go b/accessors/file/accessor_freebsd.go new file mode 100644 index 000000000..dd22a7bdb --- /dev/null +++ b/accessors/file/accessor_freebsd.go @@ -0,0 +1,69 @@ +//go:build freebsd +// +build freebsd + +/* + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package file + +import ( + "syscall" + "time" +) + +// On Linux we need xstat() support to get birth time. +func (self *OSFileInfo) Btime() time.Time { + return time.Time{} +} + +func (self *OSFileInfo) Mtime() time.Time { + ts := int64(self._Sys().Mtimespec.Sec) + return time.Unix(ts, 0) +} + +func (self *OSFileInfo) Ctime() time.Time { + ts := int64(self._Sys().Ctimespec.Sec) + return time.Unix(ts, 0) +} + +func (self *OSFileInfo) Atime() time.Time { + ts := int64(self._Sys().Atimespec.Sec) + return time.Unix(ts, 0) +} + +func splitDevNumber(dev uint64) (major, minor uint64) { + // See freebsd-src/sys/sys/types.h + major = ((dev >> 32) & 0xffffff00) | ((dev >> 8) & 0xff) + minor = ((dev >> 24) & 0xff00) | (dev & 0xffff00ff) + return +} + +func getFSType(path string) string { + var st syscall.Statfs_t + if err := syscall.Statfs(path, &st); err != nil { + return "" + } + var name []byte + for _, c := range st.Fstypename { + if c == 0 { + break + } + name = append(name, byte(c)) + } + return string(name) +} diff --git a/accessors/file/accessor_linux.go b/accessors/file/accessor_linux.go new file mode 100644 index 000000000..fd87c304a --- /dev/null +++ b/accessors/file/accessor_linux.go @@ -0,0 +1,160 @@ +//go:build linux +// +build linux + +/* + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package file + +import ( + "fmt" + "strings" + "syscall" + "time" +) + +// On Linux we need xstat() support to get birth time. +func (self *OSFileInfo) Btime() time.Time { + return time.Time{} +} + +func (self *OSFileInfo) Mtime() time.Time { + ts := int64(self._Sys().Mtim.Sec) + return time.Unix(ts, 0) +} + +func (self *OSFileInfo) Ctime() time.Time { + ts := int64(self._Sys().Ctim.Sec) + return time.Unix(ts, 0) +} + +func (self *OSFileInfo) Atime() time.Time { + ts := int64(self._Sys().Atim.Sec) + return time.Unix(ts, 0) +} + +func splitDevNumber(dev uint64) (major, minor uint64) { + // See bits/sysmacros.h (glibc) or sys/sysmacros.h (musl-libc) + major = ((dev >> 32) & 0xfffff000) | ((dev >> 8) & 0xfff) + minor = ((dev >> 12) & 0xffffff00) | (dev & 0xff) + return +} + +var fsMagic = map[uint32]string{ + // Taken from statfs(2) and/or Linux sources + 0xadf5: "ADFS", + 0xadff: "AFFS", + 0x5346414f: "AFS", + 0x0187: "AUTOFS", + 0x62646576: "BDEVFS", + 0x42465331: "BEFS", + 0x1badface: "BFS", + 0x42494e4d: "BINFMTFS", + 0xcafe4a11: "BPF_FS", + 0x9123683e: "BTRFS", + 0x73727279: "BTRFS_TEST", + 0x00c36400: "CEPH", + 0x27e0eb: "CGROUP", + 0x63677270: "CGROUP2", + 0xff534d42: "CIFS", + 0x73757245: "CODA", + 0x012ff7b7: "COH", + 0x28cd3d45: "CRAMFS", + 0x64626720: "DEBUGFS", + 0x1373: "DEVFS", + 0x1cd1: "DEVPTS", + 0xf15f: "ECRYPTFS", + 0xde5e81e4: "EFIVARFS", + 0x00414a53: "EFS", + 0x137d: "EXT", + 0xef51: "EXT2_OLD", + 0xef53: "EXT2", + 0xf2f52010: "F2FS", + 0x65735546: "FUSE", + 0xbad1dea: "FUTEXFS", + 0x4244: "HFS", + 0x00c0ffee: "HOSTFS", + 0xf995e849: "HPFS", + 0x958458f6: "HUGETLBFS", + 0x9660: "ISOFS", + 0x72b6: "JFFS2", + 0x3153464a: "JFS", + 0x137f: "MINIX", + 0x138f: "MINIX2", + 0x2468: "MINIX2", + 0x2478: "MINIX2", + 0x4d5a: "MINIX3", + 0x19800202: "MQUEUE", + 0x4d44: "MSDOS", + 0x2011BAB0: "EXFAT", + 0x11307854: "MTD_INODE_FS", + 0x564c: "NCP", + 0x6969: "NFS", + 0x3434: "NILFS", + 0x6e736673: "NSFS", + 0x5346544e: "NTFS", + 0x7461636f: "OCFS2", + 0x9fa1: "OPENPROM", + 0x794c7630: "OVERLAYFS", + 0x50495045: "PIPEFS", + 0x9fa0: "PROC", + 0x6165676c: "PSTOREFS", + 0x002f: "QNX4", + 0x68191122: "QNX6", + 0x858458f6: "RAMFS", + 0x52654973: "REISERFS", + 0x7275: "ROMFS", + 0x73636673: "SECURITYFS", + 0xf97cff8c: "SELINUX", + 0x43415d53: "SMACK", + 0x517b: "SMB", + 0xfe534d42: "SMB2", + 0x534f434b: "SOCKFS", + 0x73717368: "SQUASHFS", + 0x62656572: "SYSFS", + 0x012ff7b6: "SYSV2", + 0x012ff7b5: "SYSV4", + 0x01021994: "TMPFS", + 0x74726163: "TRACEFS", + 0x15013346: "UDF", + 0x00011954: "UFS", + 0x9fa2: "USBDEVICE", + 0x01021997: "V9FS", + 0xa501fcf5: "VXFS", + 0xabba1974: "XENFS", + 0x012ff7b4: "XENIX", + 0x58465342: "XFS", + 0x012fd16d: "XIAFS", + + 0xabababab: "VMBLOCK", + + // virtualbox/src/VBox/Additions/linux/sharedfolders/vfsmod.c + 0x786f4256: "VBOX", +} + +func getFSType(path string) string { + var st syscall.Statfs_t + if err := syscall.Statfs(path, &st); err != nil { + return "" + } + if magic, ok := fsMagic[uint32(st.Type)]; ok { + return strings.ToLower(magic) + } else { + return fmt.Sprintf("0x%08x", magic) + } +} diff --git a/accessors/file/accessor_linux_test.go b/accessors/file/accessor_linux_test.go new file mode 100644 index 000000000..1e8cc574a --- /dev/null +++ b/accessors/file/accessor_linux_test.go @@ -0,0 +1,100 @@ +//go:build linux +// +build linux + +package file + +import ( + "context" + "log" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/Velocidex/ordereddict" + "github.com/stretchr/testify/suite" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/config" + "www.velocidex.com/golang/velociraptor/glob" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils/tempfile" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" +) + +type AccessorLinuxTestSuite struct { + suite.Suite + tmpdir string +} + +func (self *AccessorLinuxTestSuite) TestLinuxSymlinks() { + tmpdir, err := tempfile.TempDir("accessor_test") + assert.NoError(self.T(), err) + + // Create two symlinks. + // tmp/second_bin/ -> tmp/zbin + // tmp/zbin -> /bin/ + + err = os.Symlink("/bin", filepath.Join(tmpdir, "zbin")) + assert.NoError(self.T(), err) + + err = os.Symlink(filepath.Join(tmpdir, "zbin"), + filepath.Join(tmpdir, "second_bin")) + assert.NoError(self.T(), err) + + // Create a symlink cycle: + // tmp/subdir is a directory + // tmp/sym1 -> tmp/subdir + // tmp/subdir/sym2 -> tmp + + dirname := filepath.Join(tmpdir, "subdir") + err = os.Mkdir(dirname, 0777) + assert.NoError(self.T(), err) + + err = os.Mkdir(filepath.Join(dirname, "ls"), 0777) + assert.NoError(self.T(), err) + + err = os.Symlink(dirname, filepath.Join(tmpdir, "sym1")) + assert.NoError(self.T(), err) + + err = os.Symlink(tmpdir, filepath.Join(dirname, "sym2")) + assert.NoError(self.T(), err) + + scope := vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{})) + scope.SetLogger(log.New(os.Stderr, " ", 0)) + + glob_path, _ := accessors.NewLinuxOSPath("/**/ls") + tmp_path, _ := accessors.NewLinuxOSPath(tmpdir) + + options := glob.GlobOptions{ + DoNotFollowSymlinks: false, + } + globber := glob.NewGlobber().WithOptions(options) + defer globber.Close() + + globber.Add(glob_path) + + accessor, err := accessors.GetAccessor("file", scope) + assert.NoError(self.T(), err) + + config_obj := config.GetDefaultConfig() + hits := []string{} + for hit := range globber.ExpandWithContext( + context.Background(), scope, + config_obj, tmp_path, accessor) { + hits = append(hits, hit.OSPath().TrimComponents( + tmp_path.Components...).String()) + } + + sort.Strings(hits) + + goldie.Assert(self.T(), "TestLinuxSymlinks", json.MustMarshalIndent(hits)) +} + +// Test Linux specific File accessor. +func TestFileLinux(t *testing.T) { + suite.Run(t, &AccessorLinuxTestSuite{}) +} diff --git a/accessors/file/accessor_test.go b/accessors/file/accessor_test.go new file mode 100644 index 000000000..bf2f70f79 --- /dev/null +++ b/accessors/file/accessor_test.go @@ -0,0 +1,196 @@ +package file_test + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/Velocidex/ordereddict" + "github.com/stretchr/testify/suite" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/config" + "www.velocidex.com/golang/velociraptor/glob" + "www.velocidex.com/golang/velociraptor/utils/tempfile" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + + _ "www.velocidex.com/golang/velociraptor/accessors/ntfs" +) + +type AccessorWindowsTestSuite struct { + suite.Suite + tmpdir string +} + +func (self *AccessorWindowsTestSuite) SetupTest() { + tmpdir, err := tempfile.TempDir("accessor_test") + assert.NoError(self.T(), err) + + self.tmpdir = strings.ReplaceAll(tmpdir, "\\", "/") +} + +func (self *AccessorWindowsTestSuite) TearDownTest() { + os.RemoveAll(self.tmpdir) // clean up +} + +func (self *AccessorWindowsTestSuite) TestACL() { + scope := vql_subsystem.MakeScope() + scope.SetLogger(log.New(os.Stderr, " ", 0)) + + accessor, err := accessors.GetAccessor("file", scope) + // Permission denied! + assert.Error(self.T(), err) + + // Try again with more premissions. + scope = vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{})) + scope.SetLogger(log.New(os.Stderr, " ", 0)) + + accessor, err = accessors.GetAccessor("file", scope) + assert.NoError(self.T(), err) + + _, err = accessor.ReadDir("/") + assert.NoError(self.T(), err) +} + +// This test will just pass on Windows in any case but will fail on +// linux if the file_nocase is broken. +func (self *AccessorWindowsTestSuite) TestNoCase() { + dirname := filepath.Join(self.tmpdir, "some/test/directory/with/parent") + err := os.MkdirAll(dirname, 0777) + assert.NoError(self.T(), err) + + file_path := filepath.Join(dirname, "1.txt") + fd, err := os.OpenFile(file_path, + os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) + assert.NoError(self.T(), err) + fd.Write([]byte("Hello world")) + fd.Close() + + scope := vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{})) + scope.SetLogger(log.New(os.Stderr, " ", 0)) + accessor, err := accessors.GetAccessor("file_nocase", scope) + assert.NoError(self.T(), err) + + // Open the file with many different casing. Mismatched casing at + // deeper directories should also work. + for _, filename := range []string{ + "%s/some/test/directory/with/parent/1.txt", + "%s/some/test/directory/with/parent/1.TxT", + "%s/some/test/directory/With/parent/1.txt", + "%s/some/test/DiRectory/With/parent/1.txt", + "%s/Some/test/DiRectory/With/parent/1.txt", + } { + interpolated_path := strings.ReplaceAll( + fmt.Sprintf(filename, self.tmpdir), "\\", "\\\\") + reader, err := accessor.Open(interpolated_path) + assert.NoError(self.T(), err) + defer fd.Close() + + data := make([]byte, 100) + n, err := reader.Read(data) + assert.NoError(self.T(), err) + assert.Equal(self.T(), string(data[:n]), "Hello world") + } +} + +// This looks like +// tmpdir/subdir/1.txt +// tmpdir/subdir/link1 -> tmpdir/subdir/1.txt +// tmpdir/subdir/parent_link -> tmpdir/subdir +// tmpdir/subdir/dir_link -> tmpdir/subdir/parent_link +func (self *AccessorWindowsTestSuite) TestSymlinks() { + // This test only works on Linux and MacOS + if runtime.GOOS == "windows" { + return + } + + dirname := filepath.Join(self.tmpdir, "subdir") + err := os.Mkdir(dirname, 0777) + assert.NoError(self.T(), err) + + file_path := filepath.Join(dirname, "1.txt") + fd, err := os.OpenFile(file_path, + os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) + assert.NoError(self.T(), err) + fd.Write([]byte("Hello world")) + fd.Close() + + // Create a symlink to the file + link_path := filepath.Join(dirname, "link1") + err = os.Symlink(file_path, link_path) + assert.NoError(self.T(), err) + + // Create a recursive symlink to parent directory + parent_link_path := filepath.Join(dirname, "parent_link") + err = os.Symlink(self.tmpdir, parent_link_path) + assert.NoError(self.T(), err) + + // Create a recursive symlink to parent directory + dir_link_path := filepath.Join(dirname, "dir_link") + err = os.Symlink(parent_link_path, dir_link_path) + assert.NoError(self.T(), err) + + scope := vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{})) + scope.SetLogger(log.New(os.Stderr, " ", 0)) + accessor, err := accessors.GetAccessor("file", scope) + assert.NoError(self.T(), err) + + // Open through the link. + for _, filename := range []string{ + "%s/subdir/1.txt", + "%s/subdir/parent_link/subdir/1.txt", + "%s/subdir/dir_link/subdir/1.txt", + + // Accept a pathspec as well. + `{"Path":"%s/subdir/1.txt"}`, + } { + interpolated_path := strings.ReplaceAll( + fmt.Sprintf(filename, self.tmpdir), "\\", "\\\\") + reader, err := accessor.Open(interpolated_path) + assert.NoError(self.T(), err) + defer fd.Close() + + data := make([]byte, 100) + n, err := reader.Read(data) + assert.NoError(self.T(), err) + assert.Equal(self.T(), string(data[:n]), "Hello world") + } + + config_obj := config.GetDefaultConfig() + + // Now glob through the files - this should not lock up since + // the cycle should be detected. + globber := glob.NewGlobber() + defer globber.Close() + + glob_path, _ := accessors.NewGenericOSPath("**/*.txt") + globber.Add(glob_path) + + hits := []string{} + tmp_path, _ := accessors.NewGenericOSPath(self.tmpdir) + for hit := range globber.ExpandWithContext( + context.Background(), scope, + config_obj, tmp_path, accessor) { + hits = append(hits, strings.ReplaceAll( + strings.TrimPrefix(hit.FullPath(), self.tmpdir), "\\", "/")) + } + + assert.Equal(self.T(), + []string{"/subdir/1.txt", "/subdir/dir_link/subdir/1.txt"}, + hits) + +} + +// Test both the Windows and Linux File accessor. +func TestWindowsLinux(t *testing.T) { + suite.Run(t, &AccessorWindowsTestSuite{}) +} diff --git a/accessors/file/auto_windows.go b/accessors/file/auto_windows.go new file mode 100644 index 000000000..17670af0f --- /dev/null +++ b/accessors/file/auto_windows.go @@ -0,0 +1,264 @@ +//go:build windows +// +build windows + +// Implements an automatic fallback to NTFS accessor when +// OSFileSystemAccessor does not work. + +package file + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + "sync" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/vfilter" +) + +// Sometimes, we can open a file with the API ok, but we just can not +// read from it. This wrapper allows for switching to the ntfs parser +// after open, but if the file is not readable. The following Python +// program creates a lock for testing. The following query will force +// a re-open with the ntfs accessor: +// SELECT read_file(filename='''C:\test.exe''', +// accessor='auto', length=15) +// FROM scope() +/* +import win32file +import win32con +import win32security +import win32api +import pywintypes + +highbits=0xffff0000 #high-order 32 bits of byte range to lock + +file="C:\\test.exe" + +secur_att = win32security.SECURITY_ATTRIBUTES() +secur_att.Initialize() + +hfile=win32file.CreateFile( + file, + win32con.GENERIC_READ|win32con.GENERIC_WRITE, + win32con.FILE_SHARE_READ|win32con.FILE_SHARE_WRITE, + secur_att, + win32con.OPEN_ALWAYS, + win32con.FILE_ATTRIBUTE_NORMAL , 0 ) + +ov=pywintypes.OVERLAPPED() +win32file.LockFileEx(hfile,win32con.LOCKFILE_EXCLUSIVE_LOCK,10,highbits,ov) +win32api.Sleep(40000) +win32file.UnlockFileEx(hfile,0,highbits,ov) +hfile.Close() +*/ +type FileReaderWrapper struct { + readatter_mu sync.Mutex + accessors.ReadSeekCloser + + mu sync.Mutex + + // If set, the reader is really an ntfs reader. + switched_to_ntfs bool + path *accessors.OSPath + + owner *AutoFilesystemAccessor +} + +func (self *FileReaderWrapper) ReadAt(buf []byte, offset int64) (int, error) { + self.readatter_mu.Lock() + defer self.readatter_mu.Unlock() + + _, err := self.Seek(offset, os.SEEK_SET) + if err != nil { + return 0, err + } + + return self.Read(buf) +} + +func (self *FileReaderWrapper) Read(buf []byte) (int, error) { + self.mu.Lock() + defer self.mu.Unlock() + + n, err := self.ReadSeekCloser.Read(buf) + if err != nil && + shouldTryNTFS(self.path.Basename(), err) && + !self.switched_to_ntfs { + + // Reopen as an ntfs parsed file. + self.path = accessors.WindowsNTFSPathFromOSPath(self.path) + fd, err1 := self.owner.ntfs_delegate.OpenWithOSPath(self.path) + if err1 != nil { + return n, err + } + + // Close the old reader and substitude a new one + self.switched_to_ntfs = true + current_offset, _ := self.ReadSeekCloser.Seek(0, os.SEEK_CUR) + self.ReadSeekCloser.Close() + + fd.Seek(current_offset, os.SEEK_SET) + self.ReadSeekCloser = fd + + // Try again with the new buffer. + return fd.Read(buf) + } + return n, err +} + +type AutoFilesystemAccessor struct { + ntfs_delegate accessors.FileSystemAccessor + file_delegate accessors.FileSystemAccessor +} + +// On Windows filesystems are usually case insensitive. +func (self AutoFilesystemAccessor) GetCanonicalFilename( + path *accessors.OSPath) string { + return strings.ToLower(path.String()) +} + +func (self AutoFilesystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewWindowsOSPath(path) +} + +func (self AutoFilesystemAccessor) New(scope vfilter.Scope) (accessors.FileSystemAccessor, error) { + ntfs_base, err := accessors.GetAccessor("ntfs", scope) + if err != nil { + return nil, err + } + + os_base, err := OSFileSystemAccessor{}.New(scope) + if err != nil { + return nil, err + } + + return &AutoFilesystemAccessor{ + ntfs_delegate: ntfs_base, + file_delegate: os_base, + }, nil +} + +func (self AutoFilesystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "auto", + Description: `Automatically access the filesystem using the best method. + +On Windows, we fallback to ntfs accessor if the file is not readable or locked. +`, + Permissions: []acls.ACL_PERMISSION{acls.FILESYSTEM_READ}, + } +} + +func (self *AutoFilesystemAccessor) GetUnderlyingAPIFilename( + full_path *accessors.OSPath) (string, error) { + return full_path.PathSpec().Path, nil +} + +func (self *AutoFilesystemAccessor) ReadDirWithOSPath( + path *accessors.OSPath) ([]accessors.FileInfo, error) { + result, err := self.file_delegate.ReadDirWithOSPath(path) + if err != nil { + ntfs_path := accessors.WindowsNTFSPathFromOSPath(path) + return self.ntfs_delegate.ReadDirWithOSPath(ntfs_path) + } + return result, err +} + +func (self *AutoFilesystemAccessor) ReadDir(path string) ([]accessors.FileInfo, error) { + result, err := self.file_delegate.ReadDir(path) + if err != nil { + return self.ntfs_delegate.ReadDir(path) + } + return result, err +} + +func (self *AutoFilesystemAccessor) Open(path string) (accessors.ReadSeekCloser, error) { + pathspec, err := self.ParsePath(path) + if err != nil { + return nil, err + } + return self.OpenWithOSPath(pathspec) +} + +func (self *AutoFilesystemAccessor) OpenWithOSPath( + path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + result, err := self.file_delegate.OpenWithOSPath(path) + if err != nil && shouldTryNTFS(path.Basename(), err) { + ntfs_path := accessors.WindowsNTFSPathFromOSPath(path) + result, err1 := self.ntfs_delegate.OpenWithOSPath(ntfs_path) + if err1 != nil { + return nil, fmt.Errorf( + "%v, unable to fall back to ntfs parsing: %w", err, err1) + } + return result, err1 + } + + // Wrap the API handle in case we need to upgrade it in future + return &FileReaderWrapper{ + ReadSeekCloser: result, + path: path, + owner: self, + }, err +} + +func shouldTryNTFS(path string, err error) bool { + // Special NTFS files start with a $ + if strings.Contains(path, "\\$") || strings.HasPrefix(path, "$") { + return true + } + + // For permission denied we fallback to ntfs parsing. + if errors.Is(err, os.ErrPermission) { + return true + } + + // These are regular errors - falling back to ntfs parsing will + // not help much. + if errors.Is(err, io.ErrUnexpectedEOF) || + errors.Is(err, io.EOF) || + errors.Is(err, os.ErrClosed) { + return false + } + + // If the file does not exist using the APIs then it is unlikely + // that nts parsing will find it. + if errors.Is(err, os.ErrNotExist) { + return false + } + + // This mostly occurs on directories. + if strings.Contains(err.Error(), "Incorrect function") { + return false + } + + // Give ntfs parsing a shot - maybe it will work? + return true +} + +func (self *AutoFilesystemAccessor) Lstat(path string) (accessors.FileInfo, error) { + result, err := self.file_delegate.Lstat(path) + if err != nil { + + return self.ntfs_delegate.Lstat(path) + } + return result, err +} + +func (self *AutoFilesystemAccessor) LstatWithOSPath( + path *accessors.OSPath) (accessors.FileInfo, error) { + result, err := self.file_delegate.LstatWithOSPath(path) + if err != nil && shouldTryNTFS(path.Basename(), err) { + ntfs_path := accessors.WindowsNTFSPathFromOSPath(path) + return self.ntfs_delegate.LstatWithOSPath(ntfs_path) + } + return result, err +} + +func init() { + accessors.Register(&AutoFilesystemAccessor{}) +} diff --git a/accessors/file/cache.go b/accessors/file/cache.go new file mode 100644 index 000000000..64c46a5ee --- /dev/null +++ b/accessors/file/cache.go @@ -0,0 +1,159 @@ +//go:build windows +// +build windows + +package file + +import ( + "os" + "strings" + "sync" + "time" + + "github.com/Velocidex/ordereddict" + ntfs "www.velocidex.com/golang/go-ntfs/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/utils/files" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/windows/wmi" + "www.velocidex.com/golang/vfilter" +) + +var ( + Cache = WMICache{} + FILE_ACCESSOR_TAG = "$__file_accessor" +) + +type WMICache struct { + mu sync.Mutex + last time.Time + logical_disks []*accessors.VirtualFileInfo +} + +func (self *WMICache) maybeUpdateCache() error { + // Result is not too old - return it. + now := utils.GetTime().Now() + if self.last.Add(time.Minute).After(now) { + return nil + } + + logical_disks, err := self.realDiscoverDriveLetters() + if err != nil { + return err + } + + self.last = now + self.logical_disks = logical_disks + + return nil +} + +func (self *WMICache) DiscoverDriveLetters() ([]accessors.FileInfo, error) { + self.mu.Lock() + defer self.mu.Unlock() + + err := self.maybeUpdateCache() + if err != nil { + return nil, err + } + + var result []accessors.FileInfo + for _, i := range self.logical_disks { + result = append(result, i) + } + + return result, nil +} + +func (self *WMICache) realDiscoverDriveLetters() ([]*accessors.VirtualFileInfo, error) { + var result []*accessors.VirtualFileInfo + + shadow_volumes, err := wmi.Query( + "SELECT DeviceID, Description, VolumeName, FreeSpace, "+ + "Size, SystemName, VolumeSerialNumber "+ + "from Win32_LogicalDisk", + "ROOT\\CIMV2") + if err == nil { + for _, row := range shadow_volumes { + size := utils.GetInt64(row, "Size") + device_name, pres := row.GetString("DeviceID") + if pres { + device_path, err := accessors.NewWindowsOSPath(device_name) + if err != nil { + return nil, err + } + + err = CheckPrefix(device_path) + if err != nil { + continue + } + + result = append(result, &accessors.VirtualFileInfo{ + IsDir_: true, + Size_: size, + Data_: row, + Path: device_path, + }) + } + } + } + + return result, nil +} + +func getDeviceReader(scope vfilter.Scope, + device_name string) (accessors.ReadSeekCloser, error) { + var device_cache *ordereddict.Dict + + if !strings.HasPrefix(device_name, "\\\\") { + device_name = "\\\\.\\" + device_name + } + + device_cache_any := vql_subsystem.CacheGet(scope, FILE_ACCESSOR_TAG) + device_cache, ok := device_cache_any.(*ordereddict.Dict) + if !ok || device_cache == nil { + device_cache = ordereddict.NewDict() + vql_subsystem.CacheSet(scope, FILE_ACCESSOR_TAG, device_cache) + } + + reader_any, ok := device_cache.Get(device_name) + if ok { + reader, ok := reader_any.(accessors.ReadSeekCloser) + if ok { + return reader, nil + } + } + + defer Instrument("RawDevice")() + + file, err := os.Open(device_name) + if err != nil { + return nil, err + } + + files.Add(device_name) + // Only close the file when the scope is destroyed. + vql_subsystem.GetRootScope(scope).AddDestructor(func() { + file.Close() + files.Remove(device_name) + }) + + // Need to read the raw device in pagesize sizes + reader, err := ntfs.NewPagedReader(file, 0x10000, 100) + if err != nil { + return nil, err + } + + res := utils.NewReadSeekReaderAdapter(reader, nil) + + // Try to figure out the size - not necessary but in case we + // can we can limit readers to this size. + stat, err1 := os.Lstat(device_name) + if err1 == nil { + res.SetSize(stat.Size()) + } + + device_cache.Set(device_name, res) + + return res, nil +} diff --git a/accessors/file/fixtures/TestLinuxSymlinks.golden b/accessors/file/fixtures/TestLinuxSymlinks.golden new file mode 100644 index 000000000..a5cc295b7 --- /dev/null +++ b/accessors/file/fixtures/TestLinuxSymlinks.golden @@ -0,0 +1,6 @@ +[ + "/second_bin/ls", + "/subdir/ls", + "/subdir/sym2/subdir/ls", + "/sym1/ls" +] \ No newline at end of file diff --git a/accessors/file/instrument.go b/accessors/file/instrument.go new file mode 100644 index 000000000..a5367182f --- /dev/null +++ b/accessors/file/instrument.go @@ -0,0 +1,27 @@ +package file + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + FileHistorgram = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "file_accessor", + Help: "Latency to access ntfs parser.", + Buckets: prometheus.LinearBuckets(0.01, 0.05, 10), + }, + []string{"action"}, + ) +) + +func Instrument(access_type string) func() time.Duration { + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + FileHistorgram.WithLabelValues(access_type).Observe(v) + })) + + return timer.ObserveDuration +} diff --git a/accessors/file/json.go b/accessors/file/json.go new file mode 100644 index 000000000..b691ba57a --- /dev/null +++ b/accessors/file/json.go @@ -0,0 +1 @@ +package file diff --git a/accessors/file/os_windows.go b/accessors/file/os_windows.go new file mode 100644 index 000000000..7e89a4348 --- /dev/null +++ b/accessors/file/os_windows.go @@ -0,0 +1,388 @@ +//go:build windows +// +build windows + +/* + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +// Windows specific implementation. For windows we make a special +// virtual root directory which contains all the drives as if they are +// subdirs. For example list dir "\\" yields c:, d:, e: then we access +// each file as an absolute path: \\c:\\Windows -> c:\Windows. +package file + +import ( + "os" + "strings" + "syscall" + "time" + + "github.com/Velocidex/ordereddict" + errors "github.com/go-errors/errors" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +var ( + fileAccessorCurrentOpened = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "accessor_file_current_open", + Help: "Number of currently opened files with the file accessor.", + }) +) + +type OSFileInfo struct { + os.FileInfo + + // Empty for files but may contain data for registry and + // resident NTFS. + _full_path *accessors.OSPath + + follow_links bool +} + +func NewOSFileInfo(base os.FileInfo, path *accessors.OSPath) *OSFileInfo { + return &OSFileInfo{ + FileInfo: base, + _full_path: path, + } +} + +func (self *OSFileInfo) FullPath() string { + return self._full_path.String() +} + +func (self *OSFileInfo) OSPath() *accessors.OSPath { + return self._full_path +} + +func (self *OSFileInfo) Data() *ordereddict.Dict { + if self.IsLink() { + target, err := os.Readlink(self.FullPath()) + if err == nil { + return ordereddict.NewDict(). + Set("Link", target) + } + } + return ordereddict.NewDict() +} + +func (self *OSFileInfo) Btime() time.Time { + nsec := self.sys().CreationTime.Nanoseconds() + return time.Unix(0, nsec) +} + +func (self *OSFileInfo) Mtime() time.Time { + nsec := self.sys().LastWriteTime.Nanoseconds() + return time.Unix(0, nsec) +} + +// Windows does not provide the ctime (inode change time) using the +// APIs. +func (self *OSFileInfo) Ctime() time.Time { + nsec := self.sys().LastWriteTime.Nanoseconds() + return time.Unix(0, nsec) +} + +func (self *OSFileInfo) Atime() time.Time { + nsec := self.sys().LastAccessTime.Nanoseconds() + return time.Unix(0, nsec) +} + +func (self *OSFileInfo) IsLink() bool { + return self.Mode()&os.ModeSymlink != 0 +} + +func (self *OSFileInfo) GetLink() (*accessors.OSPath, error) { + if !self.follow_links { + return nil, errors.New("Not following links") + } + + target, err := os.Readlink(self.FullPath()) + if err != nil { + return nil, err + } + return self._full_path.Parse(target) +} + +func (self *OSFileInfo) sys() *syscall.Win32FileAttributeData { + return self.Sys().(*syscall.Win32FileAttributeData) +} + +type OSFileSystemAccessor struct { + follow_links bool + scope vfilter.Scope +} + +func (self OSFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewWindowsOSPath(path) +} + +func (self OSFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + return &OSFileSystemAccessor{ + follow_links: self.follow_links, + scope: scope, + }, nil +} + +func (self OSFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "file", + Description: `Access the filesystem using the OS API.`, + Permissions: []acls.ACL_PERMISSION{acls.FILESYSTEM_READ}, + } +} + +func (self *OSFileSystemAccessor) GetUnderlyingAPIFilename( + full_path *accessors.OSPath) (string, error) { + return full_path.PathSpec().Path, nil +} + +func (self OSFileSystemAccessor) ReadDir(path string) ( + []accessors.FileInfo, error) { + full_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(full_path) +} + +// On Windows filesystems are usually case insensitive. +func (self OSFileSystemAccessor) GetCanonicalFilename( + path *accessors.OSPath) string { + return strings.ToLower(path.String()) +} + +func (self OSFileSystemAccessor) ReadDirWithOSPath( + full_path *accessors.OSPath) ([]accessors.FileInfo, error) { + var result []accessors.FileInfo + + defer Instrument("ReadDirWithOSPath")() + + err := CheckPrefix(full_path) + if err != nil { + return nil, err + } + + // No drive part, so list all drives. + if len(full_path.Components) == 0 { + return Cache.DiscoverDriveLetters() + } + + // Add a final \ to turn path into a directory path. This is + // needed for windows since paths that do not end with a \\ + // are interpreted incorrectly. Example readdir("c:") is not + // the same as readdir("c:\\") + dir_path := full_path.String() + "\\" + + // Windows symlinks are buggy - a ReadDir() of a link to a + // directory fails and the caller needs to specially check for + // a link. Only file access through the symlink works as + // expected (e.g. if link is a symlink to C:\Program Files): + + // dir link + // 01/15/2019 03:32 AM link [c:\Program Files] + + // dir link\ + // File Not Found <-- this should work to list the content of the + // link target + + // dir link\Git + // Content of Git directory. + + // For this reason we need to take special care when reading a + // directory in case that directory itself is a link. + files, err := utils.ReadDir(dir_path) + if err != nil { + if !self.follow_links { + return nil, err + } + + // Maybe it is a symlink + link_path := full_path.String() + target, err := os.Readlink(link_path) + if err == nil { + + // Yes it is a symlink, we just recurse into + // the target + files, err = utils.ReadDir(target) + } + } + + for _, f := range files { + child_path := full_path.Append(f.Name()) + err := CheckPrefix(child_path) + if err != nil { + continue + } + + result = append(result, + &OSFileInfo{ + follow_links: self.follow_links, + FileInfo: f, + _full_path: child_path, + }) + } + return result, nil +} + +// Wrap the os.File object to keep track of open file handles. +type OSFileWrapper struct { + *os.File +} + +func (self OSFileWrapper) IsSeekable() bool { + return true +} + +func (self OSFileWrapper) Close() error { + fileAccessorCurrentOpened.Dec() + return self.File.Close() +} + +func (self OSFileSystemAccessor) Open(path string) (accessors.ReadSeekCloser, error) { + full_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + return self.OpenWithOSPath(full_path) +} + +func (self OSFileSystemAccessor) OpenWithOSPath(full_path *accessors.OSPath) ( + accessors.ReadSeekCloser, error) { + + defer Instrument("OpenWithOSPath")() + + err := CheckPrefix(full_path) + if err != nil { + return nil, err + } + + // Opening the drive letter directly produces a reader over the + // raw disk. + if len(full_path.Components) == 1 { + device_name := full_path.Components[0] + return getDeviceReader(self.scope, device_name) + } + + filename := full_path.String() + + // The API does not accept filenames with trailing \\ for an open call. + filename = strings.TrimSuffix(filename, "\\") + file, err := os.Open(filename) + if err != nil { + return nil, err + } + + fileAccessorCurrentOpened.Inc() + return OSFileWrapper{file}, err +} + +func (self *OSFileSystemAccessor) Lstat(path string) (accessors.FileInfo, error) { + defer Instrument("Lstat")() + + full_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + err = CheckPrefix(full_path) + if err != nil { + return nil, err + } + + stat, err := os.Lstat(full_path.String()) + return &OSFileInfo{ + follow_links: self.follow_links, + FileInfo: stat, + _full_path: full_path, + }, err +} + +func (self *OSFileSystemAccessor) LstatWithOSPath(full_path *accessors.OSPath) ( + accessors.FileInfo, error) { + + defer Instrument("LstatWithOSPath")() + + err := CheckPrefix(full_path) + if err != nil { + return nil, err + } + + // An Lstat of a device returns metadata about the device + if len(full_path.Components) == 1 { + devices, err := Cache.DiscoverDriveLetters() + if err != nil { + return nil, err + } + + // Find the right device information + for _, d := range devices { + if full_path.Components[0] == d.Name() { + return d, nil + } + } + return nil, utils.NotFoundError + } + + stat, err := os.Lstat(full_path.String()) + return &OSFileInfo{ + follow_links: self.follow_links, + FileInfo: stat, + _full_path: full_path, + }, err +} + +func init() { + accessors.Register(&OSFileSystemAccessor{}) + + // Windows filesystem is already case insensitive so we provide an + // alias so artifacts can work with either. + accessors.Register(accessors.DescribeAccessor( + &OSFileSystemAccessor{}, accessors.AccessorDescriptor{ + Name: "file_nocase", + Description: `Access the filesystem using the OS API.`, + Permissions: []acls.ACL_PERMISSION{acls.FILESYSTEM_READ}, + })) + + // Register a variant which allows following links - be + // careful with it - it can get stuck on loops. + accessors.Register(accessors.DescribeAccessor( + &OSFileSystemAccessor{ + follow_links: true, + }, accessors.AccessorDescriptor{ + Name: "file_links", + Description: `Access the filesystem using the OS API. +This Accessor also follows any symlinks - Note: Take care with this accessor because there may be circular links. +`, + Permissions: []acls.ACL_PERMISSION{acls.FILESYSTEM_READ}, + })) + + // We do not register the OSFileSystemAccessor directly - it + // is used through the AutoFilesystemAccessor: If we can not + // open the file with regular OS APIs we fallback to raw NTFS + // access. This is usually what we want. + json.RegisterCustomEncoder(&OSFileInfo{}, accessors.MarshalGlobFileInfo) +} diff --git a/accessors/file/security.go b/accessors/file/security.go new file mode 100644 index 000000000..9e675626f --- /dev/null +++ b/accessors/file/security.go @@ -0,0 +1,83 @@ +package file + +import ( + "sync" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/utils" +) + +var ( + mu sync.Mutex + + allowedPrefixes *utils.PrefixTree + deniedPrefixes *utils.PrefixTree + DeniedError = utils.Wrap(acls.PermissionDenied, "No accesss to filesystem path") +) + +func SetPrefixes(allowed *utils.PrefixTree, denied *utils.PrefixTree) { + mu.Lock() + defer mu.Unlock() + + allowedPrefixes = allowed + deniedPrefixes = denied +} + +func CheckPath(full_path string) error { + destination_path, err := accessors.NewNativePath(full_path) + if err != nil || len(destination_path.Components) == 0 { + return err + } + + return CheckPrefix(destination_path) +} + +func CheckPrefix(full_path *accessors.OSPath) error { + mu.Lock() + defer mu.Unlock() + + return CheckAccessForPrefixes(full_path.Components, allowedPrefixes, deniedPrefixes) +} + +func CheckAccessForPrefixes(components []string, + allowed *utils.PrefixTree, + denied *utils.PrefixTree) error { + + // Check denies first + if denied != nil { + match, denied_depth := denied.Present(components) + if match { + // If there is a more specific allow rule, then allow it, + // otherwise we deny it. + if allowed != nil { + match, allowed_depth := allowed.Present(components) + + // If the allowed prefix is longer than the denied prefix, + // then allow it. + if match && allowed_depth > denied_depth { + return nil + } + } + return DeniedError + } + } + + // All files are allowed + if allowed == nil { + return nil + } + + if len(components) == 0 { + return nil + } + + // There is only an AllowedPrefixes and no deny prefix, this means + // we deny anything not inside the AllowedPrefixes. + match, _ := allowed.Present(components) + if match { + return nil + } + + return DeniedError +} diff --git a/accessors/file/security_test.go b/accessors/file/security_test.go new file mode 100644 index 000000000..4e5c38d05 --- /dev/null +++ b/accessors/file/security_test.go @@ -0,0 +1,114 @@ +package file_test + +import ( + "testing" + + "www.velocidex.com/golang/velociraptor/accessors/file" + "www.velocidex.com/golang/velociraptor/config" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/services/sanity" + "www.velocidex.com/golang/velociraptor/vtesting/assert" +) + +func TestAccessorFileSecurity(t *testing.T) { + config_obj := config.GetDefaultConfig() + sanity_service := &sanity.SanityChecks{} + + // No security set - everything is allowed. + config_obj.Security = &config_proto.Security{} + + sanity_service.CheckSecuritySettings(config_obj) + + assert.NoError(t, file.CheckPath("/tmp/foo/bar/baz")) + assert.NoError(t, file.CheckPath("/etc/passwd")) + + // Default server configuration: + // 1. Block access to the entire filesystem + // 2. Allow access to the /tmp/ directory + config_obj.Security = &config_proto.Security{ + AllowedFileAccessorPrefix: []string{ + "/tmp", + }, + } + + sanity_service.CheckSecuritySettings(config_obj) + + assert.NoError(t, file.CheckPath("/tmp/foo/bar/baz")) + assert.Error(t, file.CheckPath("/etc/passwd")) + + // Allow access to the whole server but reject access to the + // datastore. + config_obj.Security = &config_proto.Security{ + DeniedFileAccessorPrefix: []string{ + "/opt/velociraptor", + }, + } + + sanity_service.CheckSecuritySettings(config_obj) + + assert.Error(t, file.CheckPath("/opt/velociraptor/downloads/filename.zip")) + assert.NoError(t, file.CheckPath("/etc/passwd")) + + // Allow file access to a small part of the filestore, as well as + // the /tmp/ + config_obj.Security = &config_proto.Security{ + DeniedFileAccessorPrefix: []string{ + "/opt/velociraptor", + }, + AllowedFileAccessorPrefix: []string{ + "/tmp", + "/opt/velociraptor/downloads", + }, + } + + sanity_service.CheckSecuritySettings(config_obj) + + // Backups are not allowed. + assert.Error(t, file.CheckPath("/opt/velociraptor/backups/filename.zip")) + + // Downloads are specifically allowed. + assert.NoError(t, file.CheckPath("/opt/velociraptor/downloads/filename.zip")) + + // Random locations on the server are not allowed. + assert.Error(t, file.CheckPath("/etc/passwd")) + + // Locations in /tmp are allowed. + assert.NoError(t, file.CheckPath("/tmp")) + + // Allow to read everywhere on the server, except for the file store. But also allow reading in the downloads folder. + config_obj.Security = &config_proto.Security{ + DeniedFileAccessorPrefix: []string{ + "/opt/velociraptor", + }, + AllowedFileAccessorPrefix: []string{ + "/", + "/opt/velociraptor/downloads", + }, + } + + sanity_service.CheckSecuritySettings(config_obj) + + // Backups are not allowed since they are in the file store. + assert.Error(t, file.CheckPath("/opt/velociraptor/backups/filename.zip")) + + // Downloads are specifically allowed. + assert.NoError(t, file.CheckPath("/opt/velociraptor/downloads/filename.zip")) + + // Random locations on the server are allowed. + assert.NoError(t, file.CheckPath("/etc/passwd")) + + // Locations in /tmp are allowed. + assert.NoError(t, file.CheckPath("/tmp")) + + // Deny access to the filesystem but allow access to /tmp/ + config_obj.Security = &config_proto.Security{ + AllowedFileAccessorPrefix: []string{ + "/tmp", + }, + } + + sanity_service.CheckSecuritySettings(config_obj) + + assert.Error(t, file.CheckPath("/opt/velociraptor/downloads/filename.zip")) + assert.NoError(t, file.CheckPath("/tmp/some/long/path.txt")) +} diff --git a/accessors/file_store/accessor.go b/accessors/file_store/accessor.go new file mode 100644 index 000000000..27c473052 --- /dev/null +++ b/accessors/file_store/accessor.go @@ -0,0 +1,357 @@ +package file_store + +// This implements a filesystem accessor which can be used to access +// the generic filestore. This allows us to run globs on the file +// store regardless of the specific filestore implementation. +import ( + "errors" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/file_store_file_info" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/file_store" + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/file_store/path_specs" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/uploads" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/utils/files" + + actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +type FileStoreFileSystemAccessor struct { + file_store api.FileStore + config_obj *config_proto.Config + + sparse bool +} + +func NewFileStoreFileSystemAccessor( + config_obj *config_proto.Config) *FileStoreFileSystemAccessor { + return &FileStoreFileSystemAccessor{ + file_store: file_store.GetFileStore(config_obj), + config_obj: config_obj, + } +} + +type SparseFileStoreFileSystemAccessor struct { + FileStoreFileSystemAccessor +} + +func (self SparseFileStoreFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "fs_sparse", + Description: `Provide access to the server's filestore and datastore. + +This accessor expands sparse files. Reading from a sparse region will result in zeros being returned. +`, + Permissions: []acls.ACL_PERMISSION{acls.SERVER_ADMIN}, + } +} + +func NewSparseFileStoreFileSystemAccessor( + config_obj *config_proto.Config) *SparseFileStoreFileSystemAccessor { + return &SparseFileStoreFileSystemAccessor{ + FileStoreFileSystemAccessor: FileStoreFileSystemAccessor{ + file_store: file_store.GetFileStore(config_obj), + config_obj: config_obj, + sparse: true, + }} +} + +func (self FileStoreFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "fs", + Description: `Provide access to the server's filestore and datastore. + +Many VQL plugins produce references to files stored on the server. This accessor can be used to open those files and read them. Typically references to filestore or datastore files have the "fs:" or "ds:" prefix. +`, + Permissions: []acls.ACL_PERMISSION{acls.SERVER_ADMIN}, + } +} + +func (self FileStoreFileSystemAccessor) New( + scope vfilter.Scope) (accessors.FileSystemAccessor, error) { + config_obj, ok := vql_subsystem.GetServerConfig(scope) + if !ok { + return &FileStoreFileSystemAccessor{ + file_store: self.file_store, + config_obj: self.config_obj, + sparse: self.sparse, + }, nil + } + + return &FileStoreFileSystemAccessor{ + file_store: file_store.GetFileStore(config_obj), + config_obj: config_obj, + sparse: self.sparse, + }, nil +} + +func (self FileStoreFileSystemAccessor) Lstat(filename string) ( + accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +func (self FileStoreFileSystemAccessor) LstatWithOSPath( + filename *accessors.OSPath) ( + accessors.FileInfo, error) { + + fullpath := path_specs.FromGenericComponentList(filename.Components) + err := IsFileAccessible(fullpath) + if err != nil { + return nil, err + } + + lstat, err := self.file_store.StatFile(fullpath) + if err != nil { + // If it didnt work, we try case insensitive open + corrected_path, err := getCorrectCase(self.file_store, fullpath) + if err != nil { + return nil, err + } + lstat, err = self.file_store.StatFile(corrected_path) + if err != nil { + return nil, err + } + } + + stat := file_store_file_info.NewFileStoreFileInfoWithOSPath( + self.config_obj, filename, fullpath, lstat) + + if self.sparse { + index, err := getIndex(self.config_obj, fullpath) + if err != nil { + return stat, nil + } + + if len(index.Ranges) > 0 { + run := index.Ranges[len(index.Ranges)-1] + stat.SizeOverride_ = run.OriginalOffset + run.FileLength + } + } + + return stat, nil +} + +func (self FileStoreFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewFileStorePath(path) +} + +func (self FileStoreFileSystemAccessor) ReadDir(filename string) ( + []accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(full_path) +} + +func (self FileStoreFileSystemAccessor) ReadDirWithOSPath( + filename *accessors.OSPath) ( + []accessors.FileInfo, error) { + + fullpath := path_specs.FromGenericComponentList(filename.Components) + err := IsFileAccessible(fullpath) + if err != nil { + return nil, err + } + + files, err := self.file_store.ListDirectory(fullpath) + if err != nil { + // If it didnt work, we try case insensitive + corrected_path, err := getCorrectCase(self.file_store, fullpath) + if err != nil { + return nil, err + } + + files, err = self.file_store.ListDirectory(corrected_path) + if err != nil { + return nil, err + } + } + + var result []accessors.FileInfo + for _, f := range files { + child_path := f.PathSpec() + err := IsFileAccessible(child_path) + if err != nil { + continue + } + + child := file_store_file_info.NewFileStoreFileInfo( + self.config_obj, f.PathSpec(), f) + result = append(result, child) + } + + return result, nil +} + +func (self FileStoreFileSystemAccessor) Open(filename string) ( + accessors.ReadSeekCloser, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self FileStoreFileSystemAccessor) OpenWithOSPath(filename *accessors.OSPath) ( + accessors.ReadSeekCloser, error) { + + if len(filename.Components) == 0 { + return nil, errors.New("Invalid path") + } + + var fullpath api.FSPathSpec + + // It is a data store path + if filename.PathSpec().DelegatePath == "ds:" { + ds_path := getDSPathSpec(filename) + fullpath = ds_path.AsFilestorePath() + switch ds_path.Type() { + case api.PATH_TYPE_DATASTORE_JSON: + fullpath = fullpath.SetType(api.PATH_TYPE_FILESTORE_DB_JSON) + + case api.PATH_TYPE_DATASTORE_PROTO: + fullpath = fullpath.SetType(api.PATH_TYPE_FILESTORE_DB) + } + + } else { + fullpath = path_specs.FromGenericComponentList(filename.Components) + } + + err := IsFileAccessible(fullpath) + if err != nil { + return nil, err + } + + file, err := self.openFile(fullpath) + if err != nil { + // Try to open the old protobuf style files as a fallback. + if fullpath.Type() == api.PATH_TYPE_FILESTORE_DB_JSON { + file, err = self.openFile(fullpath.SetType(api.PATH_TYPE_FILESTORE_DB)) + } + + if err != nil { + // If it didnt work, we try case insensitive open + corrected_path, err := getCorrectCase(self.file_store, fullpath) + if err != nil { + return nil, err + } + + file, err = self.openFile(corrected_path) + if err != nil { + return nil, err + } + } + } + + return file, nil +} + +func (self FileStoreFileSystemAccessor) openFile(filename api.FSPathSpec) ( + accessors.ReadSeekCloser, error) { + file, err := self.file_store.ReadFile(filename) + if err != nil { + return nil, err + } + + key := filename.AsClientPath() + files.Add(key) + + if !self.sparse { + return file, err + } + + index, err := getIndex(self.config_obj, filename) + if err != nil { + return file, nil + } + + // Wrap the file with the index. + reader_at, err := utils.NewPagedReader(&utils.RangedReader{ + ReaderAt: utils.MakeReaderAtter(file), + Index: index, + }, 0x1000, 100) + if err != nil { + return nil, err + } + + return &ReaderWrapper{ + ReadSeekCloser: utils.NewReadSeekReaderAdapter(reader_at, func() { + files.Remove(key) + }), + Index: index, + }, nil +} + +type ReaderWrapper struct { + accessors.ReadSeekCloser + Index *actions_proto.Index +} + +// ReaderWrapper provides a Ranges() method so consumers can see +// the sparse regions. +func (self *ReaderWrapper) Ranges() (res []uploads.Range) { + for _, run := range self.Index.Ranges { + res = append(res, uploads.Range{ + Offset: run.OriginalOffset, + Length: run.Length, + IsSparse: run.FileLength == 0, + }) + } + + return res +} + +func getDSPathSpec(filename *accessors.OSPath) api.DSPathSpec { + result := path_specs.NewUnsafeDatastorePath(filename.Components...) + if len(filename.Components) > 0 { + last := len(filename.Components) - 1 + name_type, name := api.GetDataStorePathTypeFromExtension( + filename.Components[last]) + filename.Components[last] = name + return result.SetType(name_type) + } + return result +} + +// Load the index from the filestore if it is there. +func getIndex(config_obj *config_proto.Config, + vfs_path api.FSPathSpec) (*actions_proto.Index, error) { + index := &actions_proto.Index{} + + file_store_factory := file_store.GetFileStore(config_obj) + fd, err := file_store_factory.ReadFile( + vfs_path.SetType(api.PATH_TYPE_FILESTORE_SPARSE_IDX)) + if err != nil { + return nil, err + } + defer fd.Close() + + data, err := utils.ReadAllWithLimit(fd, constants.MAX_MEMORY) + if err != nil { + return nil, err + } + + err = json.Unmarshal(data, &index) + if err != nil { + return nil, err + } + + return index, nil +} diff --git a/accessors/file_store/accessor_test.go b/accessors/file_store/accessor_test.go new file mode 100644 index 000000000..63a2c20d0 --- /dev/null +++ b/accessors/file_store/accessor_test.go @@ -0,0 +1,170 @@ +package file_store_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/suite" + file_store_accessor "www.velocidex.com/golang/velociraptor/accessors/file_store" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/file_store" + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/file_store/path_specs" + "www.velocidex.com/golang/velociraptor/file_store/test_utils" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/uploads" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" +) + +type testCase struct { + checked string + err error +} + +var ( + files = []string{ + "fs:/Windows/System32/notepad.exe", + "fs:/Windows/System32/NotePad2.exe", + + // Filestores allow data to be stored in a "directory" + "fs:/Windows/System32", + } + + checked_files = []testCase{ + {checked: "fs:/WinDowS/SySteM32/NotePad.exe"}, + {checked: "fs:/Windows/System32/NotePad2.exe"}, + + // Filestores allow data to be stored in a "directory" + {checked: "fs:/windows/system32"}, + + {checked: "fs:/Windows/System32/DoesNotExist.exe", + err: utils.NotFoundError}, + } +) + +type FSAccessorTest struct { + test_utils.TestSuite + + config_obj *config_proto.Config +} + +func (self *FSAccessorTest) TestCaseInsensitive() { + accessor := file_store_accessor.NewFileStoreFileSystemAccessor(self.ConfigObj) + + file_store_factory := file_store.GetFileStore(self.ConfigObj) + + buf := make([]byte, 100) + + // Create some files with data + for _, f := range files { + pathspec, err := accessor.ParsePath(f) + assert.NoError(self.T(), err) + + fullpath := path_specs.FromGenericComponentList(pathspec.Components) + w, err := file_store_factory.WriteFile(fullpath) + assert.NoError(self.T(), err) + w.Write([]byte("hello")) + w.Close() + + // Use the accessor to open a file directly. + fd, err := accessor.Open(f) + assert.NoError(self.T(), err) + + n, err := fd.Read(buf) + assert.NoError(self.T(), err) + assert.Equal(self.T(), n, 5) + assert.Equal(self.T(), string(buf[:n]), "hello") + } + + for _, testcase := range checked_files { + // Now open the same file with the wrong casing. + fd, err := accessor.Open(testcase.checked) + if testcase.err != nil { + assert.True(self.T(), errors.Is(err, testcase.err)) + continue + } + assert.NoError(self.T(), err) + + n, err := fd.Read(buf) + assert.NoError(self.T(), err) + assert.Equal(self.T(), n, 5) + assert.Equal(self.T(), string(buf[:n]), "hello") + } +} + +func (self *FSAccessorTest) TestSparseFiles() { + filename := path_specs.FromGenericComponentList([]string{"Test.txt"}). + SetType(api.PATH_TYPE_FILESTORE_ANY) + filename_idx := filename.SetType(api.PATH_TYPE_FILESTORE_SPARSE_IDX) + + file_store_factory := file_store.GetFileStore(self.ConfigObj) + + w, err := file_store_factory.WriteFile(filename) + assert.NoError(self.T(), err) + w.Write([]byte("HelloWorld")) + w.Close() + + // Only 10 bytes are written to the filestore. + stat_file, err := file_store_factory.StatFile(filename) + assert.NoError(self.T(), err) + assert.Equal(self.T(), stat_file.Size(), int64(10)) + + w, err = file_store_factory.WriteFile(filename_idx) + assert.NoError(self.T(), err) + + // Original offset refers to the offset in the remote sparse file. + // file offset refers to the offset within the filestore file + w.Write([]byte(` +{ + "ranges": [ + { + "file_offset": 0, + "original_offset": 0, + "file_length": 5, + "length": 5 + }, + { + "file_offset": 5, + "original_offset": 5, + "length": 5, + "file_length": 0 + }, + { + "file_offset": 5, + "original_offset": 10, + "file_length": 5, + "length": 5 + } + ] +}`)) // This represents: Hello<.....>World with the gap being sparse. + w.Close() + + accessor := file_store_accessor.NewSparseFileStoreFileSystemAccessor(self.ConfigObj) + fd, err := accessor.Open(filename.Components()[0]) + assert.NoError(self.T(), err) + + buf := make([]byte, 100) + n, err := fd.Read(buf) + assert.NoError(self.T(), err) + + assert.Equal(self.T(), string(buf[:n]), "Hello\x00\x00\x00\x00\x00World") + + // Check that the file handle can report its ranges + fd_ranges, ok := fd.(uploads.RangeReader) + assert.True(self.T(), ok) + + // An Lstat() reports the sparse file size as 15 - including the sparse hole. + stat, err := accessor.Lstat(filename.Components()[0]) + assert.NoError(self.T(), err) + + assert.Equal(self.T(), stat.Size(), int64(15)) + + goldie.Assert(self.T(), "TestSparseFiles", + json.MustMarshalIndent(fd_ranges.Ranges())) +} + +func TestFileStoreAccessor(t *testing.T) { + suite.Run(t, &FSAccessorTest{}) +} diff --git a/accessors/file_store/casing.go b/accessors/file_store/casing.go new file mode 100644 index 000000000..445effb39 --- /dev/null +++ b/accessors/file_store/casing.go @@ -0,0 +1,52 @@ +package file_store + +import ( + "strings" + + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/utils" +) + +// Correct the filename to its correct casing. +func getCorrectCase( + file_store api.FileStore, + filename api.FSPathSpec) (api.FSPathSpec, error) { + + // File is exactly fine. + _, err := file_store.StatFile(filename) + if err == nil { + return filename, nil + } + + // File is not found, try to find the correct casing for the base + // component. + basename := filename.Base() + dirname := filename.Dir() + + // For non root directories we need to look at the parents + if len(dirname.Components()) > 0 { + _, err := file_store.StatFile(dirname) + if err != nil { + // The parent directory can not be directly opened - it is + // possible that the parent directory casing is incorrect too. + dirname, err = getCorrectCase(file_store, dirname) + if err != nil { + return nil, err + } + } + } + + // Try again with the correct case. It should work this time. + entries, err := file_store.ListDirectory(dirname) + if err != nil { + return nil, err + } + + for _, e := range entries { + if strings.EqualFold(e.Name(), basename) { + return dirname.AddChild(e.Name()), nil + } + } + + return nil, utils.NotFoundError +} diff --git a/accessors/file_store/fixtures/TestSparseFiles.golden b/accessors/file_store/fixtures/TestSparseFiles.golden new file mode 100644 index 000000000..eb8fb9944 --- /dev/null +++ b/accessors/file_store/fixtures/TestSparseFiles.golden @@ -0,0 +1,17 @@ +[ + { + "Offset": 0, + "Length": 5, + "IsSparse": false + }, + { + "Offset": 5, + "Length": 5, + "IsSparse": true + }, + { + "Offset": 10, + "Length": 5, + "IsSparse": false + } +] \ No newline at end of file diff --git a/accessors/file_store/permissions.go b/accessors/file_store/permissions.go new file mode 100644 index 000000000..545941f40 --- /dev/null +++ b/accessors/file_store/permissions.go @@ -0,0 +1,39 @@ +package file_store + +import ( + "sync" + + "www.velocidex.com/golang/velociraptor/accessors/file" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/utils" +) + +var ( + mu sync.Mutex + + // By default all filestore access is allowed. + allowedPrefixes *utils.PrefixTree + deniedPrefixes *utils.PrefixTree + + DeniedError = utils.Wrap(acls.PermissionDenied, "No accesss to file store path") +) + +func SetPrefixes(allowed *utils.PrefixTree, denied *utils.PrefixTree) { + mu.Lock() + defer mu.Unlock() + + allowedPrefixes = allowed + deniedPrefixes = denied +} + +// Some parts of the filestore are blocked off from reading. This +// helps prevent circumvention of the ACL system by reading files +// directly from disk. +func IsFileAccessible(filename api.FSPathSpec) error { + mu.Lock() + defer mu.Unlock() + + return file.CheckAccessForPrefixes( + filename.Components(), allowedPrefixes, deniedPrefixes) +} diff --git a/accessors/file_store/permissions_test.go b/accessors/file_store/permissions_test.go new file mode 100644 index 000000000..dd83d3295 --- /dev/null +++ b/accessors/file_store/permissions_test.go @@ -0,0 +1,45 @@ +package file_store_test + +import ( + "testing" + + "www.velocidex.com/golang/velociraptor/accessors/file_store" + "www.velocidex.com/golang/velociraptor/config" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/paths" + "www.velocidex.com/golang/velociraptor/services/sanity" + "www.velocidex.com/golang/velociraptor/vtesting/assert" +) + +func TestFSAccessorSecurity(t *testing.T) { + config_obj := config.GetDefaultConfig() + sanity_service := &sanity.SanityChecks{} + + // No security set - everything is allowed. + config_obj.Security = &config_proto.Security{ + DeniedFsAccessorPrefix: []string{ + "XXXXX", + }, + } + + sanity_service.CheckSecuritySettings(config_obj) + + assert.NoError(t, file_store.IsFileAccessible(paths.BACKUPS_ROOT.AddChild("File"))) + assert.NoError(t, file_store.IsFileAccessible(paths.PUBLIC_ROOT.AddChild("C.123"))) + + // Block access to sensitive locations + config_obj.Security = &config_proto.Security{ + DeniedFsAccessorPrefix: []string{ + "backups", + "config", + }, + } + + sanity_service.CheckSecuritySettings(config_obj) + + assert.Error(t, file_store.IsFileAccessible(paths.BACKUPS_ROOT.AddChild("File"))) + assert.NoError(t, file_store.IsFileAccessible(paths.PUBLIC_ROOT.AddChild("C.123"))) + assert.NoError(t, file_store.IsFileAccessible(paths.DOWNLOADS_ROOT.AddChild( + "C.123", "somefile.zip"))) + +} diff --git a/accessors/file_store_file_info/file_info.go b/accessors/file_store_file_info/file_info.go new file mode 100644 index 000000000..756660edc --- /dev/null +++ b/accessors/file_store_file_info/file_info.go @@ -0,0 +1,154 @@ +package file_store_file_info + +import ( + "errors" + "os" + "time" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" +) + +func NewFileStoreFileInfoWithOSPath( + config_obj *config_proto.Config, + ospath *accessors.OSPath, + fullpath api.FSPathSpec, + info os.FileInfo) *FileStoreFileInfo { + return &FileStoreFileInfo{ + FileInfo: info, + ospath: ospath, + fullpath: fullpath, + config_obj: config_obj, + } +} + +func NewFileStoreFileInfo( + config_obj *config_proto.Config, + fullpath api.FSPathSpec, + info os.FileInfo) *FileStoreFileInfo { + + // Create an OSPath to represent the abstract filestore path. + // Restore the file extension from the filestore abstract + // pathspec. + components := utils.CopySlice(fullpath.Components()) + if len(components) > 0 { + last_idx := len(components) - 1 + components[last_idx] += api.GetExtensionForFilestore(fullpath) + } + ospath := accessors.MustNewFileStorePath("fs:").Append(components...) + + return &FileStoreFileInfo{ + config_obj: config_obj, + FileInfo: info, + fullpath: fullpath, + ospath: ospath, + } +} + +type FileStoreFileInfo struct { + os.FileInfo + ospath *accessors.OSPath + fullpath api.FSPathSpec + config_obj *config_proto.Config + Data_ *ordereddict.Dict + + SizeOverride_ int64 +} + +func (self FileStoreFileInfo) Size() int64 { + if self.SizeOverride_ == 0 { + return self.FileInfo.Size() + } + return self.SizeOverride_ +} + +// We return multiple files as the base (for example the json file and +// the index both have the same basename) +func (self FileStoreFileInfo) Name() string { + return self.fullpath.Base() +} + +// This reports the unique basename +func (self FileStoreFileInfo) UniqueName() string { + return self.ospath.Basename() +} + +func (self *FileStoreFileInfo) Data() *ordereddict.Dict { + if self.Data_ == nil { + return ordereddict.NewDict() + } + + return self.Data_ +} + +// The FullPath contains the full URL to access the filestore. +func (self *FileStoreFileInfo) FullPath() string { + return self.ospath.String() +} + +func (self *FileStoreFileInfo) OSPath() *accessors.OSPath { + return self.ospath +} + +func (self *FileStoreFileInfo) PathSpec() api.FSPathSpec { + return self.fullpath +} + +func (self *FileStoreFileInfo) Btime() time.Time { + return time.Time{} +} + +func (self *FileStoreFileInfo) Mtime() time.Time { + return self.FileInfo.ModTime() +} + +func (self *FileStoreFileInfo) Ctime() time.Time { + return time.Time{} +} + +func (self *FileStoreFileInfo) Atime() time.Time { + return time.Time{} +} + +func (self *FileStoreFileInfo) IsLink() bool { + return self.Mode()&os.ModeSymlink != 0 +} + +// Filestores do not implementat links +func (self *FileStoreFileInfo) GetLink() (*accessors.OSPath, error) { + return nil, errors.New("Not implemented") +} + +func (self *FileStoreFileInfo) MarshalJSON() ([]byte, error) { + result, err := json.Marshal(&struct { + FullPath string + Size int64 + Mode os.FileMode + ModeStr string + ModTime time.Time + Btime time.Time + Mtime time.Time + Ctime time.Time + Atime time.Time + }{ + FullPath: self.FullPath(), + Size: self.Size(), + Mode: self.Mode(), + ModeStr: self.Mode().String(), + ModTime: self.ModTime(), + Btime: self.Btime(), + Mtime: self.Mtime(), + Ctime: self.Ctime(), + Atime: self.Atime(), + }) + + return result, err +} + +func (self *FileStoreFileInfo) UnmarshalJSON(data []byte) error { + return nil +} diff --git a/accessors/fixtures/TestOSPathHumanString.golden b/accessors/fixtures/TestOSPathHumanString.golden new file mode 100644 index 000000000..4afa799fb --- /dev/null +++ b/accessors/fixtures/TestOSPathHumanString.golden @@ -0,0 +1,4 @@ +{ + "Deep Pathspec": "/shared/mnt/flat -\u003e /122683392 -\u003e Windows\\System32\\Config\\SYSTEM -\u003e /ControlSet001", + "Normal path": "C:\\Windows\\System32" +} \ No newline at end of file diff --git a/accessors/fixtures/TestOSPathOperationsAppendComponents.golden b/accessors/fixtures/TestOSPathOperationsAppendComponents.golden new file mode 100644 index 000000000..9b96c8447 --- /dev/null +++ b/accessors/fixtures/TestOSPathOperationsAppendComponents.golden @@ -0,0 +1,4 @@ +{ + "Simple Path": "C:\\Windows\\System32\\notepad.exe", + "Complex Pathspec": "{\"DelegateAccessor\":\"raw_ntfs\",\"Delegate\":{\"DelegateAccessor\":\"file\",\"DelegatePath\":\"/mnt/flat\",\"Path\":\"/Windows/System32/Config/SYSTEM\"},\"Path\":\"ControlSet001\\\\Foo\\\\Bar\"}" +} \ No newline at end of file diff --git a/accessors/fixtures/TestOSPathOperationsTrimComponents.golden b/accessors/fixtures/TestOSPathOperationsTrimComponents.golden new file mode 100644 index 000000000..0a6001e82 --- /dev/null +++ b/accessors/fixtures/TestOSPathOperationsTrimComponents.golden @@ -0,0 +1,6 @@ +{ + "Simple Path": "Windows\\System32", + "Simple Path Deep": "System32", + "Complex Pathspec": "{\"DelegateAccessor\":\"raw_ntfs\",\"Delegate\":{\"DelegateAccessor\":\"file\",\"DelegatePath\":\"/mnt/flat\",\"Path\":\"/Windows/System32/Config/SYSTEM\"}}", + "Complex Pathspec Deep": "{\"DelegateAccessor\":\"raw_ntfs\",\"Delegate\":{\"DelegateAccessor\":\"file\",\"DelegatePath\":\"/mnt/flat\",\"Path\":\"/Windows/System32/Config/SYSTEM\"},\"Path\":\"Foo\\\\Bar\"}" +} \ No newline at end of file diff --git a/accessors/fixtures/TestVQLParsing.golden b/accessors/fixtures/TestVQLParsing.golden new file mode 100644 index 000000000..e169087b4 --- /dev/null +++ b/accessors/fixtures/TestVQLParsing.golden @@ -0,0 +1,117 @@ +{ + "Simple Path": { + "Components": [ + "Hello", + "World" + ], + "PathSpec": { + "Path": "/Hello/World" + } + }, + "Path With {": { + "Components": [ + "Hello", + "{this is a test}" + ], + "PathSpec": { + "Path": "/Hello/{this is a test}" + } + }, + "FSPathSpec": { + "Components": [ + "Hello", + "World.json" + ], + "PathSpec": { + "DelegateAccessor": "fs", + "DelegatePath": "fs:", + "Path": "Hello/World.json" + } + }, + "FSPathSpec With type": { + "Components": [ + "Hello", + "World.zip" + ], + "PathSpec": { + "DelegateAccessor": "fs", + "DelegatePath": "fs:", + "Path": "Hello/World.zip" + } + }, + "DSPathSpec": { + "Components": [ + "Hello", + "World.json.db" + ], + "PathSpec": { + "DelegateAccessor": "fs", + "DelegatePath": "ds:", + "Path": "Hello/World.json.db" + } + }, + "DSPathSpec With Type": { + "Components": [ + "Hello", + "World.db" + ], + "PathSpec": { + "DelegateAccessor": "fs", + "DelegatePath": "ds:", + "Path": "Hello/World.db" + } + }, + "OSPath": { + "Components": [ + "foo", + "bar" + ], + "PathSpec": { + "Path": "/foo/bar" + } + }, + "PathSpec": { + "Components": [ + "foo", + "bar" + ], + "PathSpec": { + "Path": "/foo/bar" + } + }, + "Serialized PathSpec": { + "Components": [ + "foo", + "bar.txt" + ], + "PathSpec": { + "DelegateAccessor": "file", + "DelegatePath": "/tmp/file.zip", + "Path": "/foo/bar.txt" + } + }, + "Multiple parts of mixed type": { + "Components": [ + "root", + "home", + "foo", + "bar", + "Hello.txt" + ], + "PathSpec": { + "Path": "/root/home/foo/bar/Hello.txt" + } + }, + "Multiple parts of mixed type 2": { + "Components": [ + "root", + "home", + "a", + "b", + "Hello.txt" + ], + "PathSpec": { + "Path": "/root/home/a/b/Hello.txt" + } + } +} \ No newline at end of file diff --git a/accessors/fixtures/TestVirtualFileInfo.golden b/accessors/fixtures/TestVirtualFileInfo.golden new file mode 100644 index 000000000..a6b0acece --- /dev/null +++ b/accessors/fixtures/TestVirtualFileInfo.golden @@ -0,0 +1,13 @@ +[ + { + "FullPath": "/foo", + "Size": 0, + "Mode": 2147484141, + "ModeStr": "drwxr-xr-x", + "ModTime": "0001-01-01T00:00:00Z", + "Data": {}, + "Mtime": "0001-01-01T00:00:00Z", + "Ctime": "0001-01-01T00:00:00Z", + "Atime": "0001-01-01T00:00:00Z" + } +] \ No newline at end of file diff --git a/accessors/json.go b/accessors/json.go new file mode 100644 index 000000000..aac9e58ab --- /dev/null +++ b/accessors/json.go @@ -0,0 +1,37 @@ +package accessors + +import ( + "os" + "time" + + "www.velocidex.com/golang/velociraptor/json" +) + +func MarshalGlobFileInfo(v interface{}, opts *json.EncOpts) ([]byte, error) { + self, ok := v.(FileInfo) + if !ok { + return nil, json.EncoderCallbackSkip + } + + return json.MarshalWithOptions(&struct { + FullPath string + Size int64 + Mode os.FileMode + ModeStr string + ModTime time.Time + Data interface{} + Mtime time.Time + Ctime time.Time + Atime time.Time + }{ + FullPath: self.FullPath(), + Size: self.Size(), + Mode: self.Mode(), + ModeStr: self.Mode().String(), + ModTime: self.ModTime(), + Mtime: self.Mtime(), + Ctime: self.Ctime(), + Atime: self.Atime(), + Data: self.Data(), + }, opts) +} diff --git a/accessors/manager.go b/accessors/manager.go new file mode 100644 index 000000000..2f3fe8c35 --- /dev/null +++ b/accessors/manager.go @@ -0,0 +1,152 @@ +package accessors + +import ( + "fmt" + "sync" + + errors "github.com/go-errors/errors" + + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +var ( + mu sync.Mutex + + // A global device manager is used to register handles. + globalDeviceManager *DefaultDeviceManager = NewDefaultDeviceManager() +) + +// A device manager is a factory for creating accessors. +type DeviceManager interface { + GetAccessor(scheme string, scope vfilter.Scope) (FileSystemAccessor, error) + Copy() DeviceManager + Clear() + Register(accessor FileSystemAccessor) +} + +func GetManager(scope vfilter.Scope) DeviceManager { + manager_any, pres := scope.Resolve(constants.SCOPE_DEVICE_MANAGER) + if pres { + manager, ok := manager_any.(DeviceManager) + if ok { + return manager + } + } + + config_obj, ok := vql_subsystem.GetServerConfig(scope) + if !ok { + return globalDeviceManager.Copy() + } + + return GetDefaultDeviceManager(config_obj) +} + +func GetDefaultDeviceManager(config_obj *config_proto.Config) DeviceManager { + mu.Lock() + defer mu.Unlock() + + return globalDeviceManager +} + +func GetAccessor(scheme string, scope vfilter.Scope) (FileSystemAccessor, error) { + // Fallback to the file handler - this should work + // because there needs to be at least a file handler + // registered. + switch scheme { + + case "": + scheme = "auto" + + case "reg": + // Backwards compatibility uses old shortname for reg + // accessor. + scheme = "registry" + } + + return GetManager(scope).GetAccessor(scheme, scope) +} + +// The default device manager is global and uses the +type DefaultDeviceManager struct { + mu sync.Mutex + handlers map[string]FileSystemAccessor +} + +func NewDefaultDeviceManager() *DefaultDeviceManager { + return &DefaultDeviceManager{ + handlers: make(map[string]FileSystemAccessor), + } +} + +func (self *DefaultDeviceManager) GetAccessor( + scheme string, scope vfilter.Scope) (FileSystemAccessor, error) { + + self.mu.Lock() + handler, pres := self.handlers[scheme] + self.mu.Unlock() + + if pres { + // Check permissions for accessing this handler. + for _, p := range handler.Describe().Permissions { + err := vql_subsystem.CheckAccess(scope, p) + if err != nil { + return nil, fmt.Errorf("Accessor %v: %w", scheme, err) + } + } + + res, err := handler.New(scope) + return res, err + } + return nil, errors.New("Unknown filesystem accessor " + scheme) +} + +func (self *DefaultDeviceManager) Register(accessor FileSystemAccessor) { + self.mu.Lock() + defer self.mu.Unlock() + + desc := accessor.Describe() + self.handlers[desc.Name] = accessor +} + +func (self *DefaultDeviceManager) DescribeAccessors() (res []*AccessorDescriptor) { + self.mu.Lock() + defer self.mu.Unlock() + + for _, h := range self.handlers { + res = append(res, h.Describe()) + } + return res +} + +func (self *DefaultDeviceManager) Clear() { + self.mu.Lock() + defer self.mu.Unlock() + + self.handlers = make(map[string]FileSystemAccessor) +} + +func (self *DefaultDeviceManager) Copy() DeviceManager { + self.mu.Lock() + defer self.mu.Unlock() + + return self.copy() +} + +func (self *DefaultDeviceManager) copy() *DefaultDeviceManager { + result := NewDefaultDeviceManager() + for k, v := range self.handlers { + result.handlers[k] = v + } + return result +} + +func Register(accessor FileSystemAccessor) { + globalDeviceManager.Register(accessor) +} + +func DescribeAccessors() []*AccessorDescriptor { + return globalDeviceManager.DescribeAccessors() +} diff --git a/accessors/manipulators.go b/accessors/manipulators.go new file mode 100644 index 000000000..49a0244cd --- /dev/null +++ b/accessors/manipulators.go @@ -0,0 +1,750 @@ +package accessors + +import ( + "fmt" + "regexp" + "runtime" + "strings" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" +) + +var ( + osPathSerializations = promauto.NewCounter(prometheus.CounterOpts{ + Name: "ospath_serialization_count", + Help: "Number of times an os path is serialized.", + }) + + osPathUnserializations = promauto.NewCounter(prometheus.CounterOpts{ + Name: "ospath_unserialization_count", + Help: "Number of times an os path is unserialized.", + }) +) + +// This is a generic Path manipulator that implements the escaping +// standard as used by Velociraptor: +// 1. Path separators are / but will be able to use \\ to parse. +// 2. Each component is optionally quoted if it contains special +// characters (like path separators). +type GenericPathManipulator struct { + Sep string +} + +func (self GenericPathManipulator) ComponentEqual(a, b string) bool { + return a == b +} + +func (self GenericPathManipulator) PathParse(path string, result *OSPath) error { + osPathUnserializations.Inc() + + err := maybeParsePathSpec(path, result) + if err != nil { + return err + } + result.Components = utils.SplitComponents(result.pathspec.Path) + return nil +} + +func (self GenericPathManipulator) AsPathSpec(path *OSPath) *PathSpec { + // Make a copy of the pathspec. + var result PathSpec + if path.pathspec != nil { + result = *path.pathspec + } + + components := path.Components + sep := self.Sep + if sep == "" { + sep = "/" + } + result.Path = utils.JoinComponents(components, sep) + return &result +} + +func (self GenericPathManipulator) PathJoin(path *OSPath) string { + osPathSerializations.Inc() + + result := self.AsPathSpec(path) + if result.GetDelegateAccessor() == "" && result.GetDelegatePath() == "" { + return result.Path + } + return result.String() +} + +// Like NewGenericOSPath but panics if an error +func MustNewGenericOSPath(path string) *OSPath { + res, err := NewGenericOSPath(path) + if err != nil { + panic(err) + } + return res +} + +func MustNewGenericOSPathWithBackslashSeparator(path string) *OSPath { + manipulator := GenericPathManipulator{Sep: "\\"} + result := &OSPath{ + Manipulator: manipulator, + } + + err := manipulator.PathParse(path, result) + if err != nil { + panic(err) + } + return result +} + +func NewGenericOSPath(path string) (*OSPath, error) { + manipulator := GenericPathManipulator{Sep: "/"} + result := &OSPath{ + Manipulator: manipulator, + } + + err := manipulator.PathParse(path, result) + return result, err +} + +// Responsible for serialization of linux paths +type LinuxPathManipulator struct{ GenericPathManipulator } + +func (self LinuxPathManipulator) PathParse(path string, result *OSPath) error { + osPathUnserializations.Inc() + + err := maybeParsePathSpec(path, result) + if err != nil { + return err + } + path = result.pathspec.Path + + components := strings.Split(path, "/") + result.Components = make([]string, 0, len(components)) + for _, c := range components { + if c == "" || c == "." || c == ".." { + continue + } + result.Components = append(result.Components, c) + } + return nil +} + +func (self LinuxPathManipulator) PathJoin(path *OSPath) string { + osPathSerializations.Inc() + + result := self.AsPathSpec(path) + if result.GetDelegateAccessor() == "" && result.GetDelegatePath() == "" { + return result.Path + } + return result.String() +} + +func (self LinuxPathManipulator) AsPathSpec(path *OSPath) *PathSpec { + result := path.pathspec + if result == nil { + result = &PathSpec{} + path.pathspec = result + } else { + result = result.Copy() + } + result.Path = "/" + strings.Join(path.Components, "/") + return result +} + +func MustNewLinuxOSPath(path string) *OSPath { + res, err := NewLinuxOSPath(path) + if err != nil { + panic(err) + } + return res +} + +func NewLinuxOSPath(path string) (*OSPath, error) { + manipulator := LinuxPathManipulator{} + result := &OSPath{ + pathspec: &PathSpec{}, + Manipulator: manipulator, + } + + err := manipulator.PathParse(path, result) + return result, err +} + +var ( + // For convenience we transform paths like c:\Windows -> \\.\c:\Windows + driveRegex = regexp.MustCompile( + `(?i)^[/\\]?([a-z]:)(.*)`) + + // https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats#unc-paths + uncRegex = regexp.MustCompile( + `(?i)^(\\\\[^\\]+)\\(.*)`) + + deviceDriveRegex = regexp.MustCompile( + `(?i)^(\\\\[\?\.]\\[a-zA-Z]:)(.*)`) + + deviceDirectoryRegex = regexp.MustCompile( + `(?i)^(\\\\[\?\.]\\GLOBALROOT\\Device\\[^/\\]+)([/\\]?.*)`) +) + +// Breaks a client path into components. The client's path may consist +// of a drive letter or a device which will be treated as a single +// component. For example: +// C:\Windows -> "C:\", "Windows" +// \\.\c:\Windows -> "\\.\C:", "Windows" + +// We also support UNC paths like: +// \\hostname\path\to\file -> "\\hostname", "path", "to", "file" + +// Other components that contain path separators need to be properly +// quoted as usual: +// HKEY_LOCAL_MACHINE\Software\Microsoft\"http://www.google.com"\Foo -> +// "HKEY_LOCAL_MACHINE", "Software", "Microsoft", "http://www.google.com", "Foo" + +type WindowsPathManipulator struct{ GenericPathManipulator } + +func (self WindowsPathManipulator) ComponentEqual(a, b string) bool { + return strings.EqualFold(a, b) +} + +func (self WindowsPathManipulator) PathParse(path string, result *OSPath) error { + osPathUnserializations.Inc() + + err := maybeParsePathSpec(path, result) + if err != nil { + return err + } + path = result.pathspec.Path + + m := deviceDriveRegex.FindStringSubmatch(path) + if len(m) != 0 { + result.Components = append([]string{m[1]}, utils.SplitComponents(m[2])...) + return nil + } + + m = deviceDirectoryRegex.FindStringSubmatch(path) + if len(m) != 0 { + result.Components = append([]string{m[1]}, utils.SplitComponents(m[2])...) + return nil + } + + m = uncRegex.FindStringSubmatch(path) + if len(m) != 0 { + result.Components = append([]string{m[1]}, utils.SplitComponents(m[2])...) + return nil + } + + result.Components = utils.SplitComponents(path) + return nil +} + +func (self WindowsPathManipulator) PathJoin(path *OSPath) string { + osPathSerializations.Inc() + + result := self.AsPathSpec(path) + if result.GetDelegateAccessor() == "" && result.GetDelegatePath() == "" { + return result.Path + } + return result.String() +} + +func (self WindowsPathManipulator) AsPathSpec(path *OSPath) *PathSpec { + result := path.pathspec + if result == nil { + result = &PathSpec{} + path.pathspec = result + } + + // The first component is usually the drive letter or device and + // although it can contain path separators it must not be quoted + components := path.Components + + if len(components) > 0 { + // No leading \\ as first component is drive letter + result.Path = components[0] + utils.JoinComponents(components[1:], "\\") + } else { + result.Path = "" + } + return result +} + +func MustNewWindowsOSPath(path string) *OSPath { + res, err := NewWindowsOSPath(path) + if err != nil { + panic(err) + } + return res +} + +func NewWindowsOSPath(path string) (*OSPath, error) { + manipulator := WindowsPathManipulator{} + result := &OSPath{ + Manipulator: manipulator, + } + err := manipulator.PathParse(path, result) + return result, err +} + +// Handle device paths especially. +type WindowsNTFSManipulator struct{ WindowsPathManipulator } + +func (self WindowsNTFSManipulator) PathParse(path string, result *OSPath) error { + err := self.WindowsPathManipulator.PathParse(path, result) + if err != nil { + return err + } + + // Drive names are stored as devices in the ntfs accessors. So if + // a user specifies open C:\Windows, we automatically open the + // \\.\C: device + if len(result.Components) > 0 && + driveRegex.MatchString(result.Components[0]) { + // Drive names should be uppercased + result.Components[0] = "\\\\.\\" + strings.ToUpper(result.Components[0]) + } + return nil +} + +func ConvertToDevice(component string) string { + if driveRegex.MatchString(component) { + return "\\\\.\\" + strings.ToUpper(component) + } + return component +} + +func (self WindowsNTFSManipulator) AsPathSpec(path *OSPath) *PathSpec { + result := path.pathspec + if result == nil { + result = &PathSpec{} + path.pathspec = result + } else { + result = result.Copy() + } + + // The first component is usually the drive letter or device and + // although it can contain path separators it must not be quoted + components := path.Components + + switch len(components) { + case 0: + return result + + case 1: + result.Path = components[0] + + default: + // No leading \\ as first component is drive letter + result.Path = components[0] + utils.JoinComponents(components[1:], "\\") + } + return result +} + +func (self WindowsNTFSManipulator) PathJoin(path *OSPath) string { + osPathSerializations.Inc() + + result := self.AsPathSpec(path) + if result.GetDelegateAccessor() == "" && result.GetDelegatePath() == "" { + return result.Path + } + return result.String() +} + +func MustNewWindowsNTFSPath(path string) *OSPath { + res, err := NewWindowsNTFSPath(path) + if err != nil { + panic(err) + } + return res +} + +func NewWindowsNTFSPath(path string) (*OSPath, error) { + manipulator := WindowsNTFSManipulator{} + result := &OSPath{ + Manipulator: manipulator, + } + err := manipulator.PathParse(path, result) + return result, err +} + +func WindowsNTFSPathFromOSPath(path *OSPath) *OSPath { + result := &OSPath{ + Manipulator: WindowsNTFSManipulator{}, + Components: make([]string, 0, len(path.Components)), + } + + for i, component := range path.Components { + if i == 0 { + result.Components = append(result.Components, + ConvertToDevice(component)) + } else { + result.Components = append(result.Components, component) + } + } + + return result +} + +// Windows registry paths begin with a hive name. There are a number +// of abbreviations for the hive names and we want to standardize. +type WindowsRegistryPathManipulator struct{ GenericPathManipulator } + +func (self WindowsRegistryPathManipulator) AsPathSpec(path *OSPath) *PathSpec { + result := path.pathspec + if result == nil { + result = &PathSpec{} + path.pathspec = result + } + + // The first component is usually the drive letter or device and + // although it can contain path separators it must not be quoted + components := path.Components + + result.Path = strings.TrimPrefix(utils.JoinComponents(components, "\\"), "\\") + return result +} + +func (self WindowsRegistryPathManipulator) PathJoin(path *OSPath) string { + osPathSerializations.Inc() + + result := self.AsPathSpec(path) + if result.GetDelegateAccessor() == "" && result.GetDelegatePath() == "" { + return result.Path + } + return result.String() +} + +func (self WindowsRegistryPathManipulator) PathParse( + path string, result *OSPath) error { + osPathUnserializations.Inc() + + err := maybeParsePathSpec(path, result) + if err != nil { + return err + } + result.Components = utils.SplitComponents(result.pathspec.Path) + + if len(result.Components) > 0 { + // First component is usually a hive name in upper case. + hive_name := result.Components[0] + hive_name_caps := strings.ToUpper(result.Components[0]) + switch hive_name_caps { + case "HKCU": + hive_name = "HKEY_CURRENT_USER" + case "HKLM": + hive_name = "HKEY_LOCAL_MACHINE" + case "HKU": + hive_name = "HKEY_USERS" + default: + if strings.HasPrefix(hive_name, "HKEY_") { + hive_name = hive_name_caps + } + } + + result.Components[0] = hive_name + } + return nil +} + +func MustNewWindowsRegistryPath(path string) *OSPath { + res, err := NewWindowsRegistryPath(path) + if err != nil { + panic(err) + } + return res +} + +func NewWindowsRegistryPath(path string) (*OSPath, error) { + manipulator := WindowsRegistryPathManipulator{} + result := &OSPath{ + Manipulator: manipulator, + } + err := manipulator.PathParse(path, result) + return result, err +} + +// Raw pathspec paths expect the path to be a json encoded PathSpec +// object. They do not have any special interpretation of the Path +// parameter and so they do not break it up at all. These are used in +// very limited situations when we do not want to represent +// hierarchical data at all. +type PathSpecPathManipulator struct{ GenericPathManipulator } + +func (self PathSpecPathManipulator) PathParse(path string, result *OSPath) error { + osPathUnserializations.Inc() + + pathspec, err := PathSpecFromString(path) + if err != nil { + return err + } + result.pathspec = pathspec + result.Components = []string{pathspec.Path} + return nil +} + +func (self PathSpecPathManipulator) AsPathSpec(path *OSPath) *PathSpec { + result := path.pathspec + if result == nil { + result = &PathSpec{} + path.pathspec = result + } else { + result = result.Copy() + } + return result +} + +func (self PathSpecPathManipulator) PathJoin(path *OSPath) string { + osPathSerializations.Inc() + + if path.pathspec != nil { + return path.pathspec.String() + } + + if len(path.Components) == 1 { + return path.Components[0] + } + + return "" +} + +func MustNewPathspecOSPath(path string) *OSPath { + res, err := NewPathspecOSPath(path) + if err != nil { + panic(err) + } + return res +} + +func NewPathspecOSPath(path string) (*OSPath, error) { + manipulator := PathSpecPathManipulator{} + result := &OSPath{ + Manipulator: manipulator, + } + + err := manipulator.PathParse(path, result) + return result, err +} + +func maybeParsePathSpec(path string, result *OSPath) error { + if strings.HasPrefix(path, "{") { + pathspec := &PathSpec{} + err := json.Unmarshal([]byte(path), pathspec) + if err != nil { + return fmt.Errorf("While decoding pathspec: %w", err) + } + result.pathspec = pathspec + return nil + } + + result.pathspec = &PathSpec{ + Path: path, + } + return nil +} + +// Windows registry paths begin with a hive name. There are a number +// of abbreviations for the hive names and we want to standardize. +type FileStorePathManipulator struct{} + +func (self FileStorePathManipulator) ComponentEqual(a, b string) bool { + return a == b +} + +func (self FileStorePathManipulator) AsPathSpec(path *OSPath) *PathSpec { + result := path.pathspec + if result == nil { + result = &PathSpec{} + path.pathspec = result + } else { + result = result.Copy() + } + + // The first component is usually the drive letter or device and + // although it can contain path separators it must not be quoted + components := path.Components + + result.Path = strings.TrimPrefix(utils.JoinComponents(components, "/"), "/") + return result +} + +func (self FileStorePathManipulator) PathJoin(path *OSPath) string { + osPathSerializations.Inc() + + return path.pathspec.DelegatePath + utils.JoinComponents(path.Components, "/") +} + +func (self FileStorePathManipulator) PathParse( + path string, result *OSPath) error { + osPathUnserializations.Inc() + + err := maybeParsePathSpec(path, result) + if err != nil { + return err + } + result.Components = utils.SplitComponents(result.pathspec.Path) + if len(result.Components) > 0 { + if result.Components[0] == "fs:" { + result.Components = result.Components[1:] + result.pathspec = &PathSpec{ + DelegateAccessor: "fs", + DelegatePath: "fs:", + } + return nil + } + if result.Components[0] == "ds:" { + result.Components = result.Components[1:] + result.pathspec = &PathSpec{ + DelegateAccessor: "fs", + DelegatePath: "ds:", + } + return nil + } + } + + result.pathspec = &PathSpec{ + DelegateAccessor: "fs", + DelegatePath: "fs:", + } + return nil +} + +// Like NewGenericOSPath but panics if an error +func MustNewFileStorePath(path string) *OSPath { + res, err := NewFileStorePath(path) + if err != nil { + panic(err) + } + return res +} + +func NewFileStorePath(path string) (*OSPath, error) { + manipulator := &FileStorePathManipulator{} + result := &OSPath{ + Manipulator: manipulator, + } + + err := manipulator.PathParse(path, result) + return result, err +} + +// The OSPath object for raw files is unchanged - We must pass exactly +// the same form as given to the underlying filesystem APIs. On +// Windows this is some kind of device description like +// \\?\GLOBALROOT\Device\Harddisk0\DR0 for example, but we never +// attempt to parse it - just forward to the API as is. +type RawFileManipulator struct{} + +func (self RawFileManipulator) ComponentEqual(a, b string) bool { + return a == b +} + +func (self RawFileManipulator) AsPathSpec(path *OSPath) *PathSpec { + result := &PathSpec{} + if len(path.Components) == 0 { + return result + } + + result.Path = path.Components[0] + return result +} + +func (self RawFileManipulator) PathJoin(path *OSPath) string { + if len(path.Components) == 0 { + return "" + } + return path.Components[0] +} + +func (self RawFileManipulator) PathParse( + path string, result *OSPath) error { + result.Components = []string{path} + return nil +} + +func NewRawFilePath(path string) (*OSPath, error) { + manipulator := &RawFileManipulator{} + return &OSPath{ + Components: []string{path}, + Manipulator: manipulator, + }, nil +} + +// Represent files inside the zip file for the offline collector - +// Similar to LinuxPathManipulator except that extra escaping is used +// to avoid more characters. +type ZipFileManipulator struct{} + +func (self ZipFileManipulator) ComponentEqual(a, b string) bool { + return strings.EqualFold(a, b) +} + +func (self ZipFileManipulator) AsPathSpec(path *OSPath) *PathSpec { + result := path.pathspec + if result == nil { + result = &PathSpec{} + path.pathspec = result + } + components := make([]string, 0, len(path.Components)) + for _, c := range path.Components { + if c != "" { + components = append(components, utils.SanitizeStringForZip(c)) + } + } + result.Path = "/" + strings.Join(components, "/") + return result +} + +func (self ZipFileManipulator) PathJoin(path *OSPath) string { + osPathSerializations.Inc() + + result := self.AsPathSpec(path) + if result.GetDelegateAccessor() == "" && result.GetDelegatePath() == "" { + return result.Path + } + return result.String() +} + +func (self ZipFileManipulator) PathParse( + path string, result *OSPath) error { + osPathUnserializations.Inc() + + err := maybeParsePathSpec(path, result) + if err != nil { + return err + } + path = result.pathspec.Path + + components := strings.Split(path, "/") + result.Components = make([]string, 0, len(components)) + for _, c := range components { + if c == "" || c == "." || c == ".." { + continue + } + result.Components = append(result.Components, + utils.UnsanitizeComponentForZip(c)) + } + return nil +} + +func NewZipFilePath(path string) (*OSPath, error) { + manipulator := &ZipFileManipulator{} + result := &OSPath{ + Manipulator: manipulator, + } + err := manipulator.PathParse(path, result) + return result, err +} + +func MustNewZipFilePath(path string) *OSPath { + res, err := NewZipFilePath(path) + if err != nil { + panic(err) + } + return res +} + +func NewNativePath(path string) (*OSPath, error) { + if runtime.GOOS == "windows" { + return NewLinuxOSPath(path) + } else { + return NewWindowsOSPath(path) + } +} diff --git a/accessors/manipulators_test.go b/accessors/manipulators_test.go new file mode 100644 index 000000000..b19be9a01 --- /dev/null +++ b/accessors/manipulators_test.go @@ -0,0 +1,207 @@ +package accessors + +import ( + "testing" + + "www.velocidex.com/golang/velociraptor/vtesting/assert" +) + +type testcase struct { + serialized_path string + components []string + expected_path string +} + +var generic_testcases = []testcase{ + // Generic paths try to take a good guess of the path type: + // 1. Use / or \ as path separator + // 2. Quotes represent unbroken paths. + {"/bin/file\\1.txt", []string{"bin", "file", "1.txt"}, "/bin/file/1.txt"}, + + // Quotes in the filename are escaped by doubling up and enclosing + // the component with a single quote. + {"/bin/file\"1\".txt", []string{"bin", "file\"1\".txt"}, + `/bin/"file""1"".txt"`}, + + {`/bin/"file""1"".txt"`, []string{"bin", "file\"1\".txt"}, + `/bin/"file""1"".txt"`}, + + // Enclosing a path in quotes treats it as a single literal + // component. + {"/bin/\"file\\1.txt\"", []string{"bin", "file\\1.txt"}, "/bin/\"file\\1.txt\""}, +} + +func TestGenericManipulators(t *testing.T) { + for _, testcase := range generic_testcases { + path, err := NewGenericOSPath(testcase.serialized_path) + assert.NoError(t, err) + assert.Equal(t, testcase.components, path.Components) + assert.Equal(t, testcase.expected_path, path.String()) + } +} + +var linux_testcases = []testcase{ + {"/bin/ls", []string{"bin", "ls"}, "/bin/ls"}, + {"bin////ls", []string{"bin", "ls"}, "/bin/ls"}, + {"/bin/ls////", []string{"bin", "ls"}, "/bin/ls"}, + + // Files with non-path backslash characters should be parsed as + // one filename. They should also be serialized as a single file. + {"/bin/file\\1.txt", []string{"bin", "file\\1.txt"}, "/bin/file\\1.txt"}, + + // Ignore and dont support directory traversal at all + {"/bin/../../../.././../../ls", []string{"bin", "ls"}, "/bin/ls"}, + + // Can accept paths in pathspec format + {"{\"Path\":\"/bin/ls\"}", []string{"bin", "ls"}, "/bin/ls"}, +} + +func TestLinuxManipulators(t *testing.T) { + for _, testcase := range linux_testcases { + path, err := NewLinuxOSPath(testcase.serialized_path) + assert.NoError(t, err) + assert.Equal(t, testcase.components, path.Components) + assert.Equal(t, testcase.expected_path, path.String()) + } +} + +var windows_testcases = []testcase{ + {"C:\\Windows\\System32", + []string{"C:", "Windows", "System32"}, + "C:\\Windows\\System32"}, + + // We also support / as well but always serialized to \\ + {"C:/Windows/System32", + []string{"C:", "Windows", "System32"}, + "C:\\Windows\\System32"}, + + // The drive letter must have a trailing \ otherwise the API uses + // the current directory (e.g. dir C: vs dir C:\ ) + {"C:", []string{"C:"}, "C:"}, + + // Ignore and dont support directory traversal at all + {"C:\\Windows\\System32\\..\\..\\..\\..\\ls", + []string{"C:", "Windows", "System32", "ls"}, + "C:\\Windows\\System32\\ls"}, + + // Can accept paths in pathspec format + {`{"Path":"C:\\Windows\\System32"}`, []string{ + "C:", "Windows", "System32"}, "C:\\Windows\\System32"}, +} + +func TestWindowsManipulators(t *testing.T) { + for _, testcase := range windows_testcases { + path, err := NewWindowsOSPath(testcase.serialized_path) + assert.NoError(t, err) + assert.Equal(t, testcase.components, path.Components) + assert.Equal(t, testcase.expected_path, path.String()) + } +} + +var ntfs_testcases = []testcase{ + // Devices can contain \\ but it is preserved + {"\\\\.\\C:\\Windows\\System32", + []string{"\\\\.\\C:", "Windows", "System32"}, + "\\\\.\\C:\\Windows\\System32"}, + + // Devices should not have final \\ - the API requires to open + // them without a trailing \ + {"\\\\.\\C:", []string{"\\\\.\\C:"}, "\\\\.\\C:"}, + + // Handle VSS paths + {"\\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy1\\Windows", + []string{"\\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy1", "Windows"}, + "\\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy1\\Windows"}, +} + +func TestWindowsNTFSManipulators(t *testing.T) { + for _, testcase := range ntfs_testcases { + path, err := NewWindowsNTFSPath(testcase.serialized_path) + assert.NoError(t, err) + assert.Equal(t, testcase.components, path.Components) + assert.Equal(t, testcase.expected_path, path.String()) + } +} + +var registry_testcases = []testcase{ + // Registry keys can contain slashes + {"HKEY_LOCAL_MACHINE\\\"http://www.google.com\"\\Foo", + []string{"HKEY_LOCAL_MACHINE", "http://www.google.com", "Foo"}, + "HKEY_LOCAL_MACHINE\\\"http://www.google.com\"\\Foo"}, + + // Registry keys can use shortcuts + {"HKLM\\\"http://www.google.com\"\\Foo", + []string{"HKEY_LOCAL_MACHINE", "http://www.google.com", "Foo"}, + "HKEY_LOCAL_MACHINE\\\"http://www.google.com\"\\Foo"}, +} + +func TestRegistryManipulators(t *testing.T) { + for _, testcase := range registry_testcases { + path, err := NewWindowsRegistryPath(testcase.serialized_path) + assert.NoError(t, err) + assert.Equal(t, testcase.components, path.Components) + assert.Equal(t, testcase.expected_path, path.String()) + } +} + +// Raw Pathspec OSPath do not interpret the Path parameter in a +// special way - it is just being preserved. This is only used for +// accessors that use it to represent non-hierarchical data. +var pathspec_testcases = []testcase{ + {"{\"DelegateAccessor\":\"zip\",\"DelegatePath\":\"Foo\",\"Path\":\"/bin/ls\"}", + []string{"/bin/ls"}, + "{\"DelegateAccessor\":\"zip\",\"DelegatePath\":\"Foo\",\"Path\":\"/bin/ls\"}"}, +} + +func TestPathspecManipulators(t *testing.T) { + for _, testcase := range pathspec_testcases { + path, err := NewPathspecOSPath(testcase.serialized_path) + assert.NoError(t, err) + assert.Equal(t, testcase.components, path.Components) + assert.Equal(t, testcase.expected_path, path.String()) + } +} + +// Raw Pathspec OSPath do not interpret the Path parameter in a +// special way - it is just being preserved. This is only used for +// accessors that use it to represent non-hierarchical data. +var filestore_testcases = []testcase{ + {"/clients/", []string{"clients"}, "fs:/clients"}, + {"ds:/clients/", []string{"clients"}, "ds:/clients"}, + {"fs:/clients/", []string{"clients"}, "fs:/clients"}, +} + +func TestFileStoreManipulators(t *testing.T) { + for _, testcase := range filestore_testcases { + path, err := NewFileStorePath(testcase.serialized_path) + assert.NoError(t, err) + assert.Equal(t, testcase.components, path.Components) + assert.Equal(t, testcase.expected_path, path.String()) + } +} + +// The ZipFileManipulator is used by the offline collector to abstract +// access to the collector zip files.. +var zipfile_testcases = []testcase{ + { + serialized_path: "{\"DelegateAccessor\":\"file\",\"DelegatePath\":\"/F.D4FD20VLKDJ2G.zip\",\"Path\":\"/uploads/auto/C%3A\"}", + components: []string{"uploads", "auto", "C:"}, + }, + { + serialized_path: "{\"DelegateAccessor\":\"file\",\"DelegatePath\":\"/F.D4FD20VLKDJ2G.zip\",\"Path\":\"/uploads/ntfs/%5C%5C.%5CC%3A\"}", + components: []string{"uploads", "ntfs", `\\.\C:`}, + }, +} + +func TestZipFileManipulators(t *testing.T) { + for _, testcase := range zipfile_testcases { + path, err := NewZipFilePath(testcase.serialized_path) + assert.NoError(t, err) + assert.Equal(t, testcase.components, path.Components) + expected_path := testcase.expected_path + if expected_path == "" { + expected_path = testcase.serialized_path + } + assert.Equal(t, expected_path, path.String()) + } +} diff --git a/accessors/mount.go b/accessors/mount.go new file mode 100644 index 000000000..809ffdc0e --- /dev/null +++ b/accessors/mount.go @@ -0,0 +1,391 @@ +package accessors + +/* + The mount accessor represents a filesystem built by combining other + filesystems in the same tree - i.e. "mounting" them. + + It is used to redirect various directories into multiple different + accessors. + + NOTE: Currently it is required that filesystems are mounted on + directories that exist within the containing filesystem: For example + if mounting an accessor on /usr/bin it is required that /usr/bin + exist in the root filesystem. +*/ + +import ( + "fmt" + "strings" + + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +// Mount tree is very sparse so we don't really need a map here - +// linear search is fast enough. + +// The mount accessor is essentially a redirector - it needs to find a +// delegate accessor to forward all requests to. In order to determine +// the correct delegate we walk the mount tree from the root. At each +// point in the tree we have an accessor and a prefix to prepend to +// the delegate path. + +// For example, supposed a /bin/ filesystem is mounted on /usr/. We +// have the following tree: +// root -> children = [{node: name="bin", prefix="", accessor=bin_fs_accessor] + +// To find the path /usr/bin/ls, we walk the tree from the root, find /usr/ +// as the top most delegate. However the path we need is /usr/bin/ls, +// therefore we need to access the delegate with prefix + /bin/ls +type node struct { + // The name of this node in the directory tree. + name string + + // Components of the full path in the tree from the root. + path *OSPath + + // A path prefix to apply when accessing the accessor. This allows + // us to attach a sub directory of the mounted filesystem (like a + // bind mount). + prefix *OSPath + + // The accessor to use to access. + accessor FileSystemAccessor + + // A pointer to the last mount point with an accessor. Used to + // pre-calculate the prefix and accessor for fast access. + last_mount_point *node + + // Child nodes + children []*node +} + +func (self *node) Debug() string { + res := fmt.Sprintf("node: %v, prefix: %v, accessor: %T\n", + self.name, self.prefix.String(), self.accessor) + for _, c := range self.children { + res += fmt.Sprintf(" %v\n", strings.ReplaceAll(c.Debug(), "\n", " \n")) + } + return res +} + +// Lookup a child by name. If not found returns nil +func (self *node) GetChild( + name string, manipulator PathManipulator) *node { + for _, c := range self.children { + if manipulator.ComponentEqual(c.name, name) { + return c + } + } + + return nil +} + +// Get the child node for the given name. If the node is not found, we +// create a new node based on our last mount point. +func (self *node) MakeChild( + name string, manipulator PathManipulator) *node { + for _, c := range self.children { + if manipulator.ComponentEqual(c.name, name) { + return c + } + } + + // If we get here there is no child of this name - make it based + // on the last_mount_point. + + // The full path of the new node can be derived from our own full path + new_node := &node{ + name: name, + path: self.path.Append(name), + + // This is a link up the directory tree to the last mounted + // accessor. + last_mount_point: self.last_mount_point, + accessor: self.last_mount_point.accessor, + + // The prefix to prepend to the mounted accessor is derived + // from our own prefix. + prefix: self.prefix.Append(name), + } + self.children = append(self.children, new_node) + return new_node +} + +// Our delegate accessors deal with real full paths but we want to +// pretend they are mounted inside their respective prefixes, +// therefore we need to wrap them to return the correct virtual +// fullpath. +type FileInfoWrapper struct { + FileInfo + + // This prefix will be added to all children - it reflects the + // mount path. + prefix *OSPath + remove_prefix *OSPath + + _ospath *OSPath +} + +func (self FileInfoWrapper) FullPath() string { + return self.OSPath().String() +} + +func (self FileInfoWrapper) Name() string { + return self.OSPath().Basename() +} + +func (self FileInfoWrapper) OSPath() *OSPath { + if self._ospath != nil { + return self._ospath + } + + delegate_path := self.FileInfo.OSPath() + trimmed_path := delegate_path + if self.remove_prefix != nil { + trimmed_path = delegate_path.TrimComponents( + self.remove_prefix.Components...) + } + + self._ospath = self.prefix.Append(trimmed_path.Components...) + return self._ospath +} + +func NewFileInfoWrapper(fsinfo FileInfo, + prefix, remove_prefix *OSPath) *FileInfoWrapper { + return &FileInfoWrapper{ + FileInfo: fsinfo, + prefix: prefix, + remove_prefix: remove_prefix, + } +} + +// A mount accessor maps several delegate accessors inside the same +// filesystem tree emulating mount points. +type MountFileSystemAccessor struct { + scope vfilter.Scope + + // The root filesystem is the one registered + root *node +} + +func (self *MountFileSystemAccessor) ParsePath(path string) (*OSPath, error) { + return self.root.path.Parse(path) +} + +// Walk the tree and return the last valid node that can be used to +// access the delegates as well as the residual path. +// Example: +// /usr is mounted on /mnt/ - therefore node tree will look like: +// root -> prefix: /, accessor: file, children: [ +// +// node: name: usr, accessor: file, prefix: /mnt/data, +// +// ] +// +// Now assume we access /usr/bin/ls -> We walk the tree from root: +// 1. First component is usr -> next node is child root's child. +// 2. The residual is the rest of the path which is not consumed yet +// -> i.e. "bin/ls" +// 3. Now, we can access the file as node.prefix + residual -> /mnt/data/bin/ls +func (self *MountFileSystemAccessor) getDelegateNode(os_path *OSPath) ( + *node, []string, error) { + node := self.root + + for idx, c := range os_path.Components { + if c != "" { + next_node := node.GetChild(c, os_path.Manipulator) + + // There is no internal mount point, use the last known + // mounted filesystem. + if next_node == nil { + residual := os_path.Components[idx:] + return node, residual, nil + } + + // Search deeper for a better mount point. + node = next_node + } + } + return node, nil, nil +} + +func (self MountFileSystemAccessor) Describe() *AccessorDescriptor { + return &AccessorDescriptor{} +} + +func (self *MountFileSystemAccessor) New(scope vfilter.Scope) (FileSystemAccessor, error) { + return &MountFileSystemAccessor{ + scope: scope, + root: self.root, + }, nil +} + +func (self *MountFileSystemAccessor) ReadDir(path string) ( + []FileInfo, error) { + // Parse the path into an OSPath + os_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(os_path) +} + +func (self *MountFileSystemAccessor) ReadDirWithOSPath(os_path *OSPath) ( + []FileInfo, error) { + + // delegate_node is the node we must list to get this os_path + // delegate_path is the path we must list in the node to get this os_path + delegate_node, delegate_path, err := self.getDelegatePath(os_path) + if err != nil { + return nil, err + } + children, err := delegate_node.accessor.ReadDirWithOSPath(delegate_path) + if err != nil { + return nil, err + } + + res := make([]FileInfo, 0, len(children)) + names := make([]string, 0, len(children)) + for _, c := range children { + names = append(names, c.Name()) + res = append(res, &FileInfoWrapper{ + FileInfo: c, + prefix: delegate_node.path.Copy(), + remove_prefix: delegate_node.prefix.Copy(), + }) + } + + // If we are listing the path of the delegate node, we need to add + // any children to the answer. + if os_path.Equal(delegate_node.path) { + for _, child_node := range delegate_node.children { + if utils.InString(names, child_node.name) { + continue + } + + // The child node represents a new filesystem mounted at the + // current point in the mount tree. We need to request it to + // do a stat of the prefix within its own namespace. + child_stat, err := child_node.accessor.LstatWithOSPath( + child_node.prefix) + if err == nil { + child_name := child_stat.Name() + names = append(names, child_name) + res = append(res, &FileInfoWrapper{ + FileInfo: child_stat, + prefix: child_node.path.Copy(), + remove_prefix: child_node.prefix.Copy(), + }) + } + } + } + + return res, nil +} + +func (self *MountFileSystemAccessor) getDelegatePath(path *OSPath) ( + *node, *OSPath, error) { + delegate_node, residual, err := self.getDelegateNode(path) + if err != nil { + return nil, nil, err + } + deep_delegate_path := delegate_node.prefix.Append(residual...) + return delegate_node, deep_delegate_path, nil +} + +func (self *MountFileSystemAccessor) Open(path string) (ReadSeekCloser, error) { + // Parse the path into an OSPath + os_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(os_path) +} + +func (self *MountFileSystemAccessor) OpenWithOSPath( + os_path *OSPath) (ReadSeekCloser, error) { + delegate_node, delegate_path, err := self.getDelegatePath(os_path) + if err != nil { + return nil, err + } + return delegate_node.accessor.OpenWithOSPath(delegate_path) +} + +func (self *MountFileSystemAccessor) Lstat(path string) (FileInfo, error) { + // Parse the path into an OSPath + os_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(os_path) +} + +func (self *MountFileSystemAccessor) LstatWithOSPath(os_path *OSPath) (FileInfo, error) { + delegate_node, delegate_path, err := self.getDelegatePath(os_path) + if err != nil { + return nil, err + } + file_info, err := delegate_node.accessor.LstatWithOSPath(delegate_path) + if err != nil { + return nil, err + } + + // Wrap the file info before returning it. + return &FileInfoWrapper{ + FileInfo: file_info, + prefix: delegate_node.path.Copy(), + remove_prefix: delegate_node.prefix.Copy(), + }, nil +} + +// Install a mapping from the source to the target. This means that +// operating on paths below the target will act on the +// source. Examples: +// +// source = /mnt/bin, target = /bin, accessor = file +// means Open(/bin/foo) redirects to /mnt/bin/foo with accessor "file". + +func (self *MountFileSystemAccessor) AddMapping( + source *OSPath, + target *OSPath, + source_accessor FileSystemAccessor) { + + // Walk the tree and create the sentinel node. NOTE: split the + // path according to the target accessor we are emulating. + node := self.root + + for _, c := range target.Components { + if c != "" { + node = node.MakeChild(c, target.Manipulator) + } + } + + // Install the node in the tree - this is where we read from. + node.prefix = source.Copy() + node.accessor = source_accessor + node.last_mount_point = node +} + +func NewMountFileSystemAccessor( + root_path *OSPath, root FileSystemAccessor) *MountFileSystemAccessor { + + result := &MountFileSystemAccessor{ + root: &node{ + accessor: root, + path: root_path.Copy(), + prefix: root_path.Copy(), + }, + } + result.root.last_mount_point = result.root + + return result +} + +func init() { + json.RegisterCustomEncoder(&FileInfoWrapper{}, MarshalGlobFileInfo) +} diff --git a/accessors/mount_test.go b/accessors/mount_test.go new file mode 100644 index 000000000..f0fa556b1 --- /dev/null +++ b/accessors/mount_test.go @@ -0,0 +1,139 @@ +package accessors + +import ( + "io/ioutil" + "testing" + + "www.velocidex.com/golang/velociraptor/vtesting/assert" +) + +func TestMountFilesystemAccessor(t *testing.T) { + // The root filesystem contains some directories where the other + // filesystems are mounted. + root_path := MustNewLinuxOSPath("") + + root_fs_accessor := NewVirtualFilesystemAccessor(root_path) + root_fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/usr"), + &VirtualFileInfo{ + IsDir_: true, + }) + + root_fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/home"), &VirtualFileInfo{ + IsDir_: true, + }) + root_fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/lib/foo"), &VirtualFileInfo{ + RawData: []byte("lib foo file"), + }) + + // Child filesystem contains some files. + bin_fs_accessor := NewVirtualFilesystemAccessor(root_path) + bin_fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/bin/ls"), &VirtualFileInfo{ + RawData: []byte("bin ls file"), + }) + + // This will contain a deeper mount again. + bin_fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/bin/deep"), &VirtualFileInfo{ + IsDir_: true, + }) + + bin_fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/bin/foo/bar"), &VirtualFileInfo{ + RawData: []byte("bar file"), + }) + + bin_fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/bin/foo/baz_dir"), &VirtualFileInfo{ + IsDir_: true, + }) + + bin_fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/bin/foo/baz_dir/baz"), &VirtualFileInfo{ + RawData: []byte("baz file"), + }) + + // Another filesystem will be mounted deeper again + deep_fs := NewVirtualFilesystemAccessor(root_path) + deep_fs.SetVirtualDirectory( + MustNewLinuxOSPath("/Users/mic/test.txt"), &VirtualFileInfo{ + RawData: []byte("text"), + }) + + // Create a mount filesystem to organize the different + // filesystems. Use Linux path convensions. + mount_fs := NewMountFileSystemAccessor( + MustNewLinuxOSPath(""), root_fs_accessor) + + // This means the root of the bin_fs_accessor is mounted at /usr + mount_fs.AddMapping( + MustNewLinuxOSPath("/"), + MustNewLinuxOSPath("/usr"), bin_fs_accessor) + + // It is also possible to mount into a directory inside another + // filesystem. This is similar to NTFS hard links or Linux "bind" + // mounts. The following means that the tree under /home is taken + // from /bin/foo/ on the bin_fs_accessor + mount_fs.AddMapping( + MustNewLinuxOSPath("/bin/foo"), + MustNewLinuxOSPath("/home"), bin_fs_accessor) + + // Mount deep_fs inside the bin_fs_accessor mount point + mount_fs.AddMapping( + MustNewLinuxOSPath("/"), + MustNewLinuxOSPath("/usr/bin/deep"), deep_fs) + + ls := func(path string) []string { + children, err := mount_fs.ReadDir(path) + assert.NoError(t, err) + + results := []string{} + for _, c := range children { + results = append(results, c.FullPath()) + } + //fmt.Printf("ls %v -> %v\n", path, results) + return results + } + + // Listing the root filesystem + assert.Equal(t, []string{"/usr", "/home", "/lib"}, ls("/")) + assert.Equal(t, + []string{"/usr/bin/ls", "/usr/bin/deep", "/usr/bin/foo"}, + ls("/usr/bin")) + + // /usr/bin/deep/Users/mic/ is mounted twice: + // 1. /usr/bin is mounted to bin_fs_accessor + // 2. /usr/bin/deep is mounted to deep_fs + assert.Equal(t, + []string{"/usr/bin/deep/Users/mic/test.txt"}, + ls("/usr/bin/deep/Users/mic/")) + + assert.Equal(t, + []string{"/usr/bin/deep/Users/mic"}, + ls("/usr/bin/deep/Users/")) + + // Check bind mount - /home directory comes from /bin/foo + assert.Equal(t, + []string{"/home/bar", "/home/baz_dir"}, + ls("/home/")) + + assert.Equal(t, + []string{"/home/baz_dir/baz"}, + ls("/home/baz_dir")) + + // Check the file contents + cat := func(path string) string { + fd, err := mount_fs.Open(path) + assert.NoError(t, err) + + data, err := ioutil.ReadAll(fd) + assert.NoError(t, err) + + return string(data) + } + + assert.Equal(t, "text", cat("/usr/bin/deep/Users/mic/test.txt")) +} diff --git a/accessors/mscfb/mscfb_accessor.go b/accessors/mscfb/mscfb_accessor.go new file mode 100644 index 000000000..a6a5b49fb --- /dev/null +++ b/accessors/mscfb/mscfb_accessor.go @@ -0,0 +1,262 @@ +package mscfb + +import ( + "errors" + "fmt" + "os" + "runtime/debug" + "time" + + "github.com/Velocidex/go-mscfb/parser" + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/vfilter" +) + +type MscfbFileInfo struct { + entry *parser.DirectoryEntry + _full_path *accessors.OSPath +} + +func (self *MscfbFileInfo) Name() string { + return self.entry.Name +} + +func (self *MscfbFileInfo) UniqueName() string { + return self._full_path.String() +} + +func (self *MscfbFileInfo) IsDir() bool { + return self.entry.IsDir +} + +func (self *MscfbFileInfo) Data() *ordereddict.Dict { + return ordereddict.NewDict() +} + +func (self *MscfbFileInfo) FullPath() string { + return self._full_path.String() +} + +func (self *MscfbFileInfo) OSPath() *accessors.OSPath { + return self._full_path +} + +// Not supported +func (self *MscfbFileInfo) IsLink() bool { + return false +} + +func (self *MscfbFileInfo) GetLink() (*accessors.OSPath, error) { + return nil, errors.New("Not implemented") +} + +func (self *MscfbFileInfo) Mtime() time.Time { + return self.entry.Mtime +} + +func (self *MscfbFileInfo) ModTime() time.Time { + return self.entry.Mtime +} + +func (self *MscfbFileInfo) Atime() time.Time { + return time.Time{} +} + +func (self *MscfbFileInfo) Ctime() time.Time { + return self.entry.Ctime +} + +func (self *MscfbFileInfo) Btime() time.Time { + return self.entry.Ctime +} + +func (self *MscfbFileInfo) Size() int64 { + return int64(self.entry.Size) +} + +func (self *MscfbFileInfo) Mode() os.FileMode { + var result os.FileMode = 0755 + if self.IsDir() { + result |= os.ModeDir + } + return result +} + +type MscfbFileSystemAccessor struct { + scope vfilter.Scope + + // The delegate accessor we use to open the underlying volume. + accessor string + device *accessors.OSPath + + root *accessors.OSPath +} + +func (self MscfbFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "mscfb", + Description: `Parse a MSCFB file as an archive.`, + } +} + +func (self MscfbFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + // Create a new cache in the scope. + return &MscfbFileSystemAccessor{ + scope: scope, + device: self.device, + accessor: self.accessor, + root: self.root, + }, nil +} + +func (self MscfbFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewLinuxOSPath(path) +} + +func (self *MscfbFileSystemAccessor) ReadDir(path string) ( + res []accessors.FileInfo, err error) { + // Normalize the path + fullpath, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(fullpath) +} + +func (self *MscfbFileSystemAccessor) ReadDirWithOSPath( + fullpath *accessors.OSPath) (res []accessors.FileInfo, err error) { + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + result := []accessors.FileInfo{} + if len(fullpath.Components) > 0 { + return nil, errors.New("Not found error") + } + + ole_ctx, err := GetMscfbContext( + self.scope, self.device, fullpath, self.accessor) + if err != nil { + return nil, err + } + + // List the directory. + for _, info := range ole_ctx.Directories { + result = append(result, &MscfbFileInfo{ + entry: &info, + _full_path: fullpath.Append(info.Name), + }) + } + return result, nil +} + +func (self *MscfbFileSystemAccessor) Open( + path string) (res accessors.ReadSeekCloser, err error) { + + full_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self *MscfbFileSystemAccessor) OpenWithOSPath( + fullpath *accessors.OSPath) (res accessors.ReadSeekCloser, err error) { + + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + if len(fullpath.Components) != 1 { + return nil, errors.New("Not found error") + } + + ole_ctx, err := GetMscfbContext( + self.scope, self.device, fullpath, self.accessor) + if err != nil { + return nil, err + } + + // Open the device path from the root. + stream, dir, err := ole_ctx.Open(fullpath.Components[0]) + if err != nil { + return nil, err + } + + return &readAdapter{ + info: &MscfbFileInfo{ + entry: dir, + _full_path: fullpath, + }, + reader: stream, + }, nil +} + +func (self *MscfbFileSystemAccessor) Lstat( + path string) (res accessors.FileInfo, err error) { + + fullpath, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(fullpath) +} + +func (self *MscfbFileSystemAccessor) LstatWithOSPath( + fullpath *accessors.OSPath) (res accessors.FileInfo, err error) { + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + ole_ctx, err := GetMscfbContext( + self.scope, self.device, fullpath, self.accessor) + if err != nil { + return nil, err + } + + var dir *parser.DirectoryEntry + if len(fullpath.Components) > 1 { + return nil, errors.New("Not found error") + } + + if len(fullpath.Components) == 0 { + // Root directory + dir, err = ole_ctx.GetDirentry(0) + } else { + + dir, err = ole_ctx.Stat(fullpath.Components[0]) + } + + return &MscfbFileInfo{ + entry: dir, + _full_path: fullpath, + }, err +} + +func init() { + accessors.Register(&MscfbFileSystemAccessor{}) + + json.RegisterCustomEncoder(&MscfbFileInfo{}, accessors.MarshalGlobFileInfo) +} diff --git a/accessors/mscfb/reader.go b/accessors/mscfb/reader.go new file mode 100644 index 000000000..30e768ade --- /dev/null +++ b/accessors/mscfb/reader.go @@ -0,0 +1,79 @@ +package mscfb + +import ( + "fmt" + "io" + "runtime/debug" + "sync" + + "www.velocidex.com/golang/velociraptor/accessors" +) + +type readAdapter struct { + sync.Mutex + + info accessors.FileInfo + pos int64 + reader io.ReaderAt +} + +func (self *readAdapter) Read(buf []byte) (res int, err error) { + self.Lock() + defer self.Unlock() + + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + res, err = self.reader.ReadAt(buf, self.pos) + + // If ReadAt is unable to read anything it means an EOF. + if res == 0 { + // The NTFS cache may be flushed during this read and in this + // case the file handle will be closed on us during the + // read. This usually shows up as an EOF read with 0 length. + // See Issue + // https://github.com/Velocidex/velociraptor/issues/2153 + + // We catch this issue by issuing one more read just to make + // sure. Usually we are wrapping a ReadAtter here and we do + // not expect to see a EOF anyway. In the case of NTFS the + // extra read will re-open the underlying device file with a + // new NTFS context (reparsing the $MFT and purging all the + // caches) so the next read will succeed. + res, err = self.reader.ReadAt(buf, self.pos) + if res == 0 { + // Still EOF - give up + return res, io.EOF + } + } + + self.pos += int64(res) + + return res, err +} + +func (self *readAdapter) ReadAt(buf []byte, offset int64) (int, error) { + self.Lock() + defer self.Unlock() + self.pos = offset + + return self.reader.ReadAt(buf, offset) +} + +func (self *readAdapter) Close() error { + return nil +} + +func (self *readAdapter) Seek(offset int64, whence int) (int64, error) { + self.Lock() + defer self.Unlock() + + self.pos = offset + return self.pos, nil +} diff --git a/accessors/mscfb/utils.go b/accessors/mscfb/utils.go new file mode 100644 index 000000000..682e0d86b --- /dev/null +++ b/accessors/mscfb/utils.go @@ -0,0 +1,59 @@ +package mscfb + +import ( + "github.com/Velocidex/go-mscfb/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/constants" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/readers" + "www.velocidex.com/golang/vfilter" +) + +func GetMscfbContext(scope vfilter.Scope, + device, fullpath *accessors.OSPath, accessor string) ( + result *parser.OLEContext, err error) { + + if device == nil { + device, err = fullpath.Delegate(scope) + if err != nil { + return nil, err + } + accessor = fullpath.DelegateAccessor() + } + + return GetMscfbCache(scope, device, accessor) +} + +func GetMscfbCache(scope vfilter.Scope, + device *accessors.OSPath, accessor string) (*parser.OLEContext, error) { + key := "mscfb_cache" + device.String() + accessor + + // Get the cache context from the root scope's cache + cache_ctx, ok := vql_subsystem.CacheGet(scope, key).(*parser.OLEContext) + if !ok { + lru_size := vql_subsystem.GetIntFromRow( + scope, scope, constants.NTFS_CACHE_SIZE) + + paged_reader, err := readers.NewAccessorReader( + scope, accessor, device, int(lru_size)) + if err != nil { + return nil, err + } + + cache_ctx, err = parser.GetOLEContext(paged_reader) + if err != nil { + return nil, err + } + vql_subsystem.CacheSet(scope, key, cache_ctx) + + // Close the device when we are done with this query. + err = vql_subsystem.GetRootScope(scope).AddDestructor(func() { + paged_reader.Close() + }) + if err != nil { + return nil, err + } + } + + return cache_ctx, nil +} diff --git a/accessors/ntfs/cache.go b/accessors/ntfs/cache.go new file mode 100644 index 000000000..eb414ff49 --- /dev/null +++ b/accessors/ntfs/cache.go @@ -0,0 +1,141 @@ +//go:build windows +// +build windows + +package ntfs + +import ( + "sync" + "time" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/vql/windows/wmi" +) + +var ( + Cache = WMICache{} +) + +type WMICache struct { + mu sync.Mutex + last time.Time + logical_disks []*accessors.VirtualFileInfo + vss []*accessors.VirtualFileInfo +} + +func (self *WMICache) realDiscoverVSS() ([]*accessors.VirtualFileInfo, error) { + shadow_volumes, err := wmi.Query( + "SELECT DeviceObject, VolumeName, InstallDate, "+ + "OriginatingMachine from Win32_ShadowCopy", + "ROOT\\CIMV2") + if err != nil { + return nil, err + } + + result := []*accessors.VirtualFileInfo{} + for _, row := range shadow_volumes { + device_name, pres := row.GetString("DeviceObject") + if pres { + device_path, err := accessors.NewWindowsNTFSPath(device_name) + if err != nil { + return nil, err + } + virtual_directory := &accessors.VirtualFileInfo{ + IsDir_: true, + Path: device_path, + Size_: 0, // WMI does not give the original volume size + Data_: row, + } + result = append(result, virtual_directory) + } + } + + return result, nil +} + +func (self *WMICache) realDiscoverLogicalDisks() ([]*accessors.VirtualFileInfo, error) { + result := []*accessors.VirtualFileInfo{} + shadow_volumes, err := wmi.Query( + "SELECT DeviceID, Description, VolumeName, FreeSpace, "+ + "Size, SystemName, VolumeSerialNumber "+ + "from Win32_LogicalDisk WHERE FileSystem = 'NTFS'", + "ROOT\\CIMV2") + if err != nil { + return nil, err + } + + for _, row := range shadow_volumes { + device_name, pres := row.GetString("DeviceID") + if pres { + device_path, err := accessors.NewWindowsNTFSPath("\\\\.\\" + device_name) + if err != nil { + return nil, err + } + virtual_directory := &accessors.VirtualFileInfo{ + IsDir_: true, + Size_: utils.GetInt64(row, "Size"), + Path: device_path, + Data_: row, + } + result = append(result, virtual_directory) + } + } + + return result, nil +} + +func (self *WMICache) maybeUpdateCache() error { + // Result is not too old - return it. + now := utils.GetTime().Now() + if self.last.Add(time.Minute).After(now) { + return nil + } + + logical_disks, err := self.realDiscoverLogicalDisks() + if err != nil { + return err + } + + vss, err := self.realDiscoverVSS() + if err != nil { + return err + } + + self.last = now + self.logical_disks = logical_disks + self.vss = vss + + return nil +} + +func (self *WMICache) DiscoverLogicalDisks() ([]*accessors.VirtualFileInfo, error) { + self.mu.Lock() + defer self.mu.Unlock() + + err := self.maybeUpdateCache() + if err != nil { + return nil, err + } + + var result []*accessors.VirtualFileInfo + for _, r := range self.logical_disks { + result = append(result, r) + } + return result, nil +} + +func (self *WMICache) DiscoverVSS() ([]*accessors.VirtualFileInfo, error) { + self.mu.Lock() + defer self.mu.Unlock() + + err := self.maybeUpdateCache() + if err != nil { + return nil, err + } + + var result []*accessors.VirtualFileInfo + for _, r := range self.vss { + result = append(result, r) + } + return result, nil +} diff --git a/accessors/ntfs/fixtures/TestNTFSFilesystemAccessor.golden b/accessors/ntfs/fixtures/TestNTFSFilesystemAccessor.golden new file mode 100644 index 000000000..388bbc176 --- /dev/null +++ b/accessors/ntfs/fixtures/TestNTFSFilesystemAccessor.golden @@ -0,0 +1,22 @@ +[ + "$AttrDef", + "$BadClus", + "$BadClus:$Bad", + "$Bitmap", + "$Boot", + "$Extend", + "$LogFile", + "$MFT", + "$MFTMirr", + "$RECYCLE.BIN", + "$Secure", + "$Secure:$SDS", + "$UpCase", + "$UpCase:$Info", + "$Volume", + "Folder A", + "System Volume Information", + "another_file.txt", + "just_a_file.txt", + "ones.bin" +] \ No newline at end of file diff --git a/accessors/ntfs/fixtures/TestNTFSFilesystemAccessorRemapping.golden b/accessors/ntfs/fixtures/TestNTFSFilesystemAccessorRemapping.golden new file mode 100644 index 000000000..906b67788 --- /dev/null +++ b/accessors/ntfs/fixtures/TestNTFSFilesystemAccessorRemapping.golden @@ -0,0 +1,4 @@ +[ + "\\\\.\\C:\\$MFT", + "\\\\.\\D:\\$MFT" +] \ No newline at end of file diff --git a/accessors/ntfs/instrument.go b/accessors/ntfs/instrument.go new file mode 100644 index 000000000..fa946252e --- /dev/null +++ b/accessors/ntfs/instrument.go @@ -0,0 +1,27 @@ +package ntfs + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + NTFSHistorgram = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "ntfs_accessor", + Help: "Latency to access file accessor.", + Buckets: prometheus.LinearBuckets(0.01, 0.05, 10), + }, + []string{"action"}, + ) +) + +func Instrument(access_type string) func() time.Duration { + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + NTFSHistorgram.WithLabelValues(access_type).Observe(v) + })) + + return timer.ObserveDuration +} diff --git a/accessors/ntfs/mft.go b/accessors/ntfs/mft.go new file mode 100644 index 000000000..c282be680 --- /dev/null +++ b/accessors/ntfs/mft.go @@ -0,0 +1,222 @@ +/* + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +// A Raw NTFS accessor for disks. This accessor allows navigating the +// filesystem by MFT ids e.g. C:/X-Y-Z + +// The First level is the MFT ID (X) +// The Second level is the Attribute type (Y) +// The Third level is the Attribute ID. + +package ntfs + +import ( + "errors" + + ntfs "www.velocidex.com/golang/go-ntfs/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/ntfs/readers" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +type MFTFileSystemAccessor struct { + scope vfilter.Scope +} + +func (self MFTFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewWindowsNTFSPath(path) +} + +func (self MFTFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "mft", + Description: `Access arbitrary MFT streams as files.`, + } +} + +func (self MFTFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + return &MFTFileSystemAccessor{scope: scope}, nil +} + +func (self MFTFileSystemAccessor) ReadDir(path string) ( + []accessors.FileInfo, error) { + return nil, errors.New("Unable to list all MFT entries.") +} + +func (self MFTFileSystemAccessor) ReadDirWithOSPath(path *accessors.OSPath) ( + []accessors.FileInfo, error) { + return nil, errors.New("Unable to list all MFT entries.") +} + +func (self MFTFileSystemAccessor) parseMFTPath(full_path *accessors.OSPath) ( + delegate_device *accessors.OSPath, delegate_accessor string, + subpath string, err error) { + + // There are two ways to use this accessor: + + // 1. Using a pathspec we can delegate to an external file to + // parse the ntfs. Eg. {Path: "43-128-0", DelegatePath: "\\\\.\\C:"} + // 2. If a delegate is not specified, we take the device from the + // first component of the Path. + + delegate_device = accessors.MustNewWindowsNTFSPath( + full_path.Components[0]) + delegate_accessor = "file" + + // If the user provided a full pathspec we use that instead. + if full_path.DelegatePath() != "" { + delegate_device, err = full_path.Delegate(self.scope) + if err != nil { + return nil, "", "", err + } + delegate_accessor = full_path.DelegateAccessor() + subpath = full_path.Components[0] + } else if len(full_path.Components) < 2 { + return nil, "", "", utils.NotFoundError + } else { + subpath = full_path.Components[1] + } + return delegate_device, delegate_accessor, subpath, nil +} + +func (self *MFTFileSystemAccessor) Open(path string) ( + accessors.ReadSeekCloser, error) { + + full_path, err := self.ParsePath(path) + if err != nil || len(full_path.Components) == 0 { + return nil, utils.NotFoundError + } + + return self.OpenWithOSPath(full_path) +} + +func (self *MFTFileSystemAccessor) OpenWithOSPath(full_path *accessors.OSPath) ( + accessors.ReadSeekCloser, error) { + + defer Instrument("OpenWithOSPath")() + + delegate_device, delegate_accessor, subpath, err := self.parseMFTPath( + full_path) + if err != nil { + return nil, err + } + + // Check that the subpath is correctly specified. + mft_idx, attr_type, attr_id, stream_name, err := ntfs.ParseMFTId(subpath) + if err != nil { + return nil, err + } + + ntfs_ctx, err := readers.GetNTFSContext( + self.scope, delegate_device, delegate_accessor) + if err != nil { + return nil, err + } + + mft_entry, err := ntfs_ctx.GetMFT(mft_idx) + if err != nil { + return nil, err + } + + info := &ntfs.FileInfo{} + stat := ntfs.Stat(ntfs_ctx, mft_entry) + if len(stat) > 0 { + info = stat[0] + } + + // Attributes are never directories + // since they always have some data. + info.IsDir = false + + reader, err := ntfs.OpenStream(ntfs_ctx, mft_entry, + uint64(attr_type), uint16(attr_id), stream_name) + if err != nil { + return nil, err + } + + ranges := reader.Ranges() + if len(ranges) > 0 { + last_run := ranges[len(ranges)-1] + info.Size = last_run.Offset + last_run.Length + } + + result := &readAdapter{ + info: &NTFSFileInfo{ + info: info, + _full_path: full_path.Copy(), + }, + reader: reader, + } + return result, nil +} + +func (self *MFTFileSystemAccessor) Lstat(path string) ( + accessors.FileInfo, error) { + full_path, err := self.ParsePath(path) + if err != nil || len(full_path.Components) == 0 { + return nil, utils.NotFoundError + } + + return self.LstatWithOSPath(full_path) +} + +func (self *MFTFileSystemAccessor) LstatWithOSPath(full_path *accessors.OSPath) ( + accessors.FileInfo, error) { + delegate_device, delegate_accessor, subpath, err := self.parseMFTPath(full_path) + if err != nil { + return nil, err + } + + // Check that the subpath is correctly specified. + mft_idx, _, _, _, err := ntfs.ParseMFTId(subpath) + if err != nil { + return nil, err + } + + ntfs_ctx, err := readers.GetNTFSContext( + self.scope, delegate_device, delegate_accessor) + if err != nil { + return nil, err + } + + mft_entry, err := ntfs_ctx.GetMFT(mft_idx) + if err != nil { + return nil, err + } + + info := &ntfs.FileInfo{} + stat := ntfs.Stat(ntfs_ctx, mft_entry) + if len(stat) > 0 { + info = stat[0] + } + + // Attributes are never directories + // since they always have some data. + info.IsDir = false + + return &NTFSFileInfo{ + info: info, + _full_path: full_path.Copy(), + }, nil +} + +func init() { + accessors.Register(&MFTFileSystemAccessor{}) +} diff --git a/accessors/ntfs/mft_test.go b/accessors/ntfs/mft_test.go new file mode 100644 index 000000000..b873a4930 --- /dev/null +++ b/accessors/ntfs/mft_test.go @@ -0,0 +1,39 @@ +package ntfs + +import ( + "log" + "os" + "path/filepath" + "testing" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vtesting/assert" +) + +func TestMFTFilesystemAccessor(t *testing.T) { + scope := vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{})) + scope.SetLogger(log.New(os.Stderr, " ", 0)) + + abs_path, _ := filepath.Abs("../../artifacts/testdata/files/test.ntfs.dd") + fs_accessor, err := MFTFileSystemAccessor{}.New(scope) + assert.NoError(t, err) + + pathspec := accessors.MustNewPathspecOSPath(accessors.PathSpec{ + Path: "38-128-0", + DelegateAccessor: "file", + DelegatePath: abs_path, + }.String()) + + buffer := make([]byte, 40) + fd, err := fs_accessor.OpenWithOSPath(pathspec) + assert.NoError(t, err) + + _, err = fd.Read(buffer) + assert.NoError(t, err) + + assert.Equal(t, "ONESONESONESONESONESONESONESONESONESONES", string(buffer)) +} diff --git a/accessors/ntfs/ntfs_accessor.go b/accessors/ntfs/ntfs_accessor.go new file mode 100644 index 000000000..6e45e626b --- /dev/null +++ b/accessors/ntfs/ntfs_accessor.go @@ -0,0 +1,625 @@ +package ntfs + +// This is an accessor which represents an NTFS filesystem +/* + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +// A Raw NTFS accessor for disks. + +import ( + "errors" + "fmt" + "io" + "os" + "runtime/debug" + "strings" + "sync" + "time" + + "github.com/Velocidex/ordereddict" + + ntfs "www.velocidex.com/golang/go-ntfs/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/file" + "www.velocidex.com/golang/velociraptor/accessors/ntfs/readers" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/uploads" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/utils/files" + "www.velocidex.com/golang/vfilter" +) + +const ( + // Scope cache tag for the NTFS parser + NTFSFileSystemTag = "_NTFS" +) + +type NTFSFileInfo struct { + info *ntfs.FileInfo + _full_path *accessors.OSPath +} + +func (self *NTFSFileInfo) IsDir() bool { + return self.info.IsDir +} + +func (self *NTFSFileInfo) Size() int64 { + return self.info.Size +} + +func (self *NTFSFileInfo) Data() *ordereddict.Dict { + result := ordereddict.NewDict(). + Set("mft", self.info.MFTId). + Set("name_type", self.info.NameType). + Set("fn_btime", self.info.FNBtime). + Set("fn_mtime", self.info.FNMtime) + if self.info.ExtraNames != nil { + result.Set("extra_names", self.info.ExtraNames) + } + + return result +} + +func (self *NTFSFileInfo) Name() string { + return self.info.Name +} + +func (self *NTFSFileInfo) UniqueName() string { + return self._full_path.String() +} + +func (self *NTFSFileInfo) Mode() os.FileMode { + var result os.FileMode = 0755 + if self.IsDir() { + result |= os.ModeDir + } + return result +} + +func (self *NTFSFileInfo) ModTime() time.Time { + return self.info.Mtime +} + +func (self *NTFSFileInfo) FullPath() string { + return self._full_path.String() +} + +func (self *NTFSFileInfo) OSPath() *accessors.OSPath { + return self._full_path +} + +func (self *NTFSFileInfo) Btime() time.Time { + return self.info.Btime +} + +func (self *NTFSFileInfo) Mtime() time.Time { + return self.info.Mtime +} + +func (self *NTFSFileInfo) Ctime() time.Time { + return self.info.Ctime +} + +func (self *NTFSFileInfo) Atime() time.Time { + return self.info.Atime +} + +// Not supported +func (self *NTFSFileInfo) IsLink() bool { + return false +} + +func (self *NTFSFileInfo) GetLink() (*accessors.OSPath, error) { + return nil, errors.New("Not implemented") +} + +type NTFSFileSystemAccessor struct { + scope vfilter.Scope + + // The delegate accessor we use to open the underlying volume. + accessor string + device *accessors.OSPath + + root *accessors.OSPath +} + +func NewNTFSFileSystemAccessor( + scope vfilter.Scope, + root_path *accessors.OSPath, + device *accessors.OSPath, accessor string) *NTFSFileSystemAccessor { + return &NTFSFileSystemAccessor{ + scope: scope, + accessor: accessor, + device: device, + root: root_path, + } +} + +func (self NTFSFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "raw_ntfs", + Description: `Access the NTFS filesystem inside an image by parsing NTFS.`, + } +} + +func (self NTFSFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + // Create a new cache in the scope. + return &NTFSFileSystemAccessor{ + scope: scope, + device: self.device, + accessor: self.accessor, + root: self.root, + }, nil +} + +func (self *NTFSFileSystemAccessor) getRootMFTEntry(ntfs_ctx *ntfs.NTFSContext) ( + *ntfs.MFT_ENTRY, error) { + return ntfs_ctx.GetMFT(5) +} + +func (self NTFSFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewWindowsNTFSPath(path) +} + +func (self *NTFSFileSystemAccessor) ReadDir(path string) ( + res []accessors.FileInfo, err error) { + // Normalize the path + fullpath, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(fullpath) +} + +// NTFS filesystems are usually case insensitive. +func (self NTFSFileSystemAccessor) GetCanonicalFilename( + path *accessors.OSPath) string { + return strings.ToLower(path.String()) +} + +func (self *NTFSFileSystemAccessor) ReadDirWithOSPath( + fullpath *accessors.OSPath) (res []accessors.FileInfo, err error) { + + defer Instrument("ReadDirWithOSPath")() + + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + err = file.CheckPrefix(fullpath) + if err != nil { + return nil, err + } + + result := []accessors.FileInfo{} + + device := self.device + accessor := self.accessor + if device == nil { + device, err = fullpath.Delegate(self.scope) + if err != nil { + return nil, err + } + accessor = fullpath.DelegateAccessor() + } + + ntfs_ctx, err := readers.GetNTFSContext(self.scope, device, accessor) + if err != nil { + return nil, err + } + + root, err := ntfs_ctx.GetMFT(5) + if err != nil { + return nil, err + } + + // Open the device path from the root. + dir, err := Open(self.scope, root, ntfs_ctx, device, accessor, fullpath) + if err != nil { + return nil, err + } + + // Only process each mft id once. + seen := []int64{} + in_seen := func(id int64) bool { + for _, i := range seen { + if i == id { + return true + } + } + return false + } + + // List the directory. + for _, node := range dir.Dir(ntfs_ctx) { + node_mft_id := int64(node.MftReference()) + if in_seen(node_mft_id) { + continue + } + + seen = append(seen, node_mft_id) + + node_mft, err := ntfs_ctx.GetMFT(node_mft_id) + if err != nil { + continue + } + // Emit a result for each filename + for _, info := range ntfs.Stat(ntfs_ctx, node_mft) { + // Skip . files - they are pretty useless. + if info == nil || info.Name == "." || info.Name == ".." { + continue + } + result = append(result, &NTFSFileInfo{ + info: info, + _full_path: fullpath.Append(info.Name), + }) + } + } + return result, nil +} + +// Adapt a ReadSeeker onto the ReadAtter that go-ntfs provides. +type readAdapter struct { + sync.Mutex + + info accessors.FileInfo + reader ntfs.RangeReaderAt + pos int64 +} + +func (self *readAdapter) Ranges() []uploads.Range { + result := []uploads.Range{} + for _, rng := range self.reader.Ranges() { + result = append(result, uploads.Range{ + Offset: rng.Offset, + Length: rng.Length, + IsSparse: rng.IsSparse, + }) + } + return result +} + +func (self *readAdapter) Read(buf []byte) (res int, err error) { + self.Lock() + defer self.Unlock() + + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + res, err = self.reader.ReadAt(buf, self.pos) + // If ReadAt is unable to read anything it means an EOF. + if res == 0 { + // The NTFS cache may be flushed during this read and in this + // case the file handle will be closed on us during the + // read. This usually shows up as an EOF read with 0 length. + // See Issue + // https://github.com/Velocidex/velociraptor/issues/2153 + + // We catch this issue by issuing one more read just to make + // sure. Usually we are wrapping a ReadAtter here and we do + // not expect to see a EOF anyway. In the case of NTFS the + // extra read will re-open the underlying device file with a + // new NTFS context (reparsing the $MFT and purging all the + // caches) so the next read will succeed. + res, err = self.reader.ReadAt(buf, self.pos) + if res == 0 { + // Still EOF - give up + return res, io.EOF + } + } + + self.pos += int64(res) + + return res, err +} + +func (self *readAdapter) ReadAt(buf []byte, offset int64) (int, error) { + self.Lock() + defer self.Unlock() + self.pos = offset + + return self.reader.ReadAt(buf, offset) +} + +func (self *readAdapter) Close() error { + return nil +} + +func (self *readAdapter) Seek(offset int64, whence int) (int64, error) { + self.Lock() + defer self.Unlock() + + self.pos = offset + return self.pos, nil +} + +func (self *NTFSFileSystemAccessor) Open( + path string) (res accessors.ReadSeekCloser, err error) { + + full_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self *NTFSFileSystemAccessor) OpenWithOSPath( + fullpath *accessors.OSPath) (res accessors.ReadSeekCloser, err error) { + + defer Instrument("OpenWithOSPath")() + + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + err = file.CheckPrefix(fullpath) + if err != nil { + return nil, err + } + + device := self.device + accessor := self.accessor + if device == nil { + device, err = fullpath.Delegate(self.scope) + if err != nil { + return nil, err + } + accessor = fullpath.DelegateAccessor() + } + + // We dont want to open a subpath of the filesystem, instead we + // special case this as openning the raw device. + if len(fullpath.Components) == 0 { + defer Instrument("RawDevice")() + + accessor, err := accessors.GetAccessor(accessor, self.scope) + if err != nil { + return nil, err + } + + file, err := accessor.OpenWithOSPath(device) + if err != nil { + return nil, err + } + + files.Add(device.String()) + + reader, err := ntfs.NewPagedReader( + utils.MakeReaderAtter(file), 0x1000, 1000) + if err != nil { + return nil, err + } + + return utils.NewReadSeekReaderAdapter(reader, func() { + files.Remove(device.String()) + }), nil + + } + + ntfs_ctx, err := readers.GetNTFSContext(self.scope, device, accessor) + if err != nil { + return nil, err + } + + root, err := self.getRootMFTEntry(ntfs_ctx) + if err != nil { + return nil, err + } + + data, err := ntfs.GetDataForPath(ntfs_ctx, fullpath.Path()) + if err != nil { + return nil, err + } + + dirname := fullpath.Dirname() + basename := strings.ToLower(fullpath.Basename()) + + dir, err := Open(self.scope, root, ntfs_ctx, device, accessor, dirname) + if err != nil { + return nil, err + } + + for _, info := range ntfs.ListDir(ntfs_ctx, dir) { + if strings.ToLower(info.Name) == basename { + return &readAdapter{ + info: &NTFSFileInfo{ + info: info, + _full_path: dirname.Append(info.Name), + }, + reader: data, + }, nil + } + } + + return nil, errors.New("File not found") +} + +func (self *NTFSFileSystemAccessor) Lstat( + path string) (res accessors.FileInfo, err error) { + + fullpath, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(fullpath) +} + +func (self *NTFSFileSystemAccessor) LstatWithOSPath( + fullpath *accessors.OSPath) (res accessors.FileInfo, err error) { + defer func() { + r := recover() + if r != nil { + fmt.Printf("PANIC %v\n", r) + debug.PrintStack() + err, _ = r.(error) + } + }() + + err = file.CheckPrefix(fullpath) + if err != nil { + return nil, err + } + + device := self.device + accessor := self.accessor + if device == nil { + device, err = fullpath.Delegate(self.scope) + if err != nil { + return nil, err + } + accessor = fullpath.DelegateAccessor() + } + + // Attempting to stat the top level mean that we want to stat the + // device itself. + if self.device != nil && len(fullpath.Components) == 0 { + accessor_obj, err := accessors.GetAccessor(accessor, self.scope) + if err != nil { + return nil, err + } + return accessor_obj.LstatWithOSPath(self.device) + } + + ntfs_ctx, err := readers.GetNTFSContext(self.scope, device, accessor) + if err != nil { + return nil, err + } + + root, err := self.getRootMFTEntry(ntfs_ctx) + if err != nil { + return nil, err + } + + dirname := fullpath.Dirname() + basename := strings.ToLower(fullpath.Basename()) + dir, err := Open(self.scope, root, ntfs_ctx, device, accessor, dirname) + if err != nil { + return nil, err + } + for _, info := range ntfs.ListDir(ntfs_ctx, dir) { + if strings.ToLower(info.Name) == basename { + res := &NTFSFileInfo{ + info: info, + _full_path: dirname.Append(info.Name), + } + return res, nil + + } + } + + return nil, errors.New("File not found") +} + +// Open the MFT entry specified by a path name. Walks all directory +// indexes in the path to find the right MFT entry. +func Open(scope vfilter.Scope, self *ntfs.MFT_ENTRY, + ntfs_ctx *ntfs.NTFSContext, + device *accessors.OSPath, accessor string, + filename *accessors.OSPath) (*ntfs.MFT_ENTRY, error) { + + defer Instrument("Open")() + + components := filename.Components + + // Path is the relative path from the root of the device we want to list + // component: The name of the file we want (case insensitive) + // dir: The MFT entry to search. + get_path_in_dir := func(path string, component string, dir *ntfs.MFT_ENTRY) ( + *ntfs.MFT_ENTRY, error) { + + key := device.String() + path + path_cache := GetNTFSPathCache(scope, device, accessor) + item, pres := path_cache.GetComponentMetadata(key, component) + if pres { + return ntfs_ctx.GetMFT(item.MftId) + } + + lru_map := make(map[string]*CacheMFT) + + // Populate the directory cache with all the mft ids. + lower_component := strings.ToLower(component) + for _, idx_record := range dir.Dir(ntfs_ctx) { + file := idx_record.File() + name_type := file.NameType().Name + if name_type == "DOS" { + continue + } + item_name := file.Name() + mft_id := int64(idx_record.MftReference()) + + lru_map[strings.ToLower(item_name)] = &CacheMFT{ + MftId: mft_id, + Component: item_name, + NameType: name_type, + } + } + path_cache.SetLRUMap(key, lru_map) + + for _, v := range lru_map { + if strings.ToLower(v.Component) == lower_component { + return ntfs_ctx.GetMFT(v.MftId) + } + } + + return nil, errors.New("Not found") + } + + // NOTE: This refreshes each parent directory in the LRU. + directory := self + path := "" + for _, component := range components { + if component == "" { + continue + } + next, err := get_path_in_dir( + path, component, directory) + if err != nil { + return nil, err + } + directory = next + path = path + "\\" + component + } + + return directory, nil +} + +func init() { + accessors.Register(&NTFSFileSystemAccessor{}) + + json.RegisterCustomEncoder(&NTFSFileInfo{}, accessors.MarshalGlobFileInfo) +} diff --git a/accessors/ntfs/ntfs_accessor_test.go b/accessors/ntfs/ntfs_accessor_test.go new file mode 100644 index 000000000..81df1c978 --- /dev/null +++ b/accessors/ntfs/ntfs_accessor_test.go @@ -0,0 +1,131 @@ +package ntfs + +import ( + "context" + "log" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/config" + "www.velocidex.com/golang/velociraptor/glob" + "www.velocidex.com/golang/velociraptor/json" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" + + _ "www.velocidex.com/golang/velociraptor/accessors/file" +) + +func TestNTFSFilesystemAccessor(t *testing.T) { + config_obj := config.GetDefaultConfig() + scope := vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{})) + scope.SetLogger(log.New(os.Stderr, " ", 0)) + + abs_path, _ := filepath.Abs("../../artifacts/testdata/files/test.ntfs.dd") + root_path := accessors.MustNewWindowsOSPath("") + + fs_accessor := NewNTFSFileSystemAccessor( + scope, root_path, accessors.MustNewGenericOSPath(abs_path), "file") + + globber := glob.NewGlobber() + defer globber.Close() + + globber.Add(accessors.MustNewWindowsOSPath("/*")) + + hits := []string{} + for hit := range globber.ExpandWithContext( + context.Background(), scope, config_obj, root_path, fs_accessor) { + hits = append(hits, hit.OSPath().String()) + } + + goldie.Assert(t, "TestNTFSFilesystemAccessor", json.MustMarshalIndent(hits)) + + buffer := make([]byte, 40) + fd, err := fs_accessor.Open("/ones.bin") + assert.NoError(t, err) + + _, err = fd.Read(buffer) + assert.NoError(t, err) + + assert.Equal(t, "ONESONESONESONESONESONESONESONESONESONES", string(buffer)) +} + +// Here we build a remapping of the same ntfs image between two mount points. +func TestNTFSFilesystemAccessorRemapping(t *testing.T) { + config_obj := config.GetDefaultConfig() + + // Create the two mount point directories in the VirtualFilesystemAccessor + root_path := accessors.MustNewWindowsOSPath("") + root_fs_accessor := accessors.NewVirtualFilesystemAccessor(root_path) + root_fs_accessor.SetVirtualFileInfo(&accessors.VirtualFileInfo{ + Path: accessors.MustNewWindowsOSPath("\\\\.\\C:"), + IsDir_: true, + }) + + root_fs_accessor.SetVirtualFileInfo(&accessors.VirtualFileInfo{ + Path: accessors.MustNewWindowsOSPath("\\\\.\\D:"), + IsDir_: true, + }) + + // Overlay a MountFileSystemAccessor over the + // VirtualFilesystemAccessor. We will use Windows path + // convensions so it looks like a real windows system. + mount_fs := accessors.NewMountFileSystemAccessor( + accessors.MustNewWindowsOSPath(""), root_fs_accessor) + + scope := vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{})) + scope.SetLogger(log.New(os.Stderr, " ", 0)) + + abs_path, _ := filepath.Abs("../../artifacts/testdata/files/test.ntfs.dd") + c_fs_accessor := NewNTFSFileSystemAccessor( + scope, root_path, accessors.MustNewGenericOSPath(abs_path), "file") + d_fs_accessor := NewNTFSFileSystemAccessor( + scope, root_path, accessors.MustNewGenericOSPath(abs_path), "file") + + // Mount the ntfs accessors on the C and D devices + mount_fs.AddMapping( + accessors.MustNewWindowsOSPath(""), // Mount at the root of the filesystem + accessors.MustNewWindowsOSPath("\\\\.\\C:"), + c_fs_accessor) + + mount_fs.AddMapping( + accessors.MustNewWindowsOSPath(""), + accessors.MustNewWindowsOSPath("\\\\.\\D:"), + d_fs_accessor) + + // Start globbing from the top level. + // Find all $MFT files + globber := glob.NewGlobber() + defer globber.Close() + + globber.Add(accessors.MustNewWindowsOSPath("/*/$MFT")) + + hits := []string{} + for hit := range globber.ExpandWithContext( + context.Background(), scope, config_obj, accessors.MustNewWindowsOSPath(""), + mount_fs) { + hits = append(hits, hit.FullPath()) + } + + sort.Strings(hits) + + goldie.Assert(t, "TestNTFSFilesystemAccessorRemapping", + json.MustMarshalIndent(hits)) + + // Now open a file for reading. + buffer := make([]byte, 40) + fd, err := mount_fs.Open("\\\\.\\C:\\ones.bin") + assert.NoError(t, err) + + _, err = fd.Read(buffer) + assert.NoError(t, err) + + assert.Equal(t, "ONESONESONESONESONESONESONESONESONESONES", string(buffer)) +} diff --git a/accessors/ntfs/ntfs_accessor_windows.go b/accessors/ntfs/ntfs_accessor_windows.go new file mode 100644 index 000000000..f5c7b611f --- /dev/null +++ b/accessors/ntfs/ntfs_accessor_windows.go @@ -0,0 +1,145 @@ +//go:build windows +// +build windows + +package ntfs + +import ( + "context" + "time" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/file" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/constants" + "www.velocidex.com/golang/vfilter" +) + +const ( + NTFS_TAG = "$__NTFS_Accessor" +) + +type WindowsNTFSFileSystemAccessor struct { + *accessors.MountFileSystemAccessor + age time.Time +} + +func (self *WindowsNTFSFileSystemAccessor) Lstat(path string) (accessors.FileInfo, error) { + // Parse the path into an OSPath + os_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(os_path) +} + +func (self *WindowsNTFSFileSystemAccessor) LstatWithOSPath( + os_path *accessors.OSPath) (accessors.FileInfo, error) { + + defer Instrument("LstatWithOSPath")() + + err := file.CheckPrefix(os_path) + if err != nil { + return nil, err + } + + // Calling an LStat on the device shall return file info about the + // device itself (including size). e.g. Lstat("\\C:\") -> info about the volume. + if len(os_path.Components) == 1 { + // Try to match the device to the component required. This can + // be either a VSS volume or a Logical disk volume. + devices, err := Cache.DiscoverVSS() + if err != nil { + return nil, err + } + + for _, d := range devices { + if d.Name() == os_path.Components[0] { + return d, nil + } + } + devices, err = Cache.DiscoverLogicalDisks() + if err != nil { + return nil, err + } + + for _, d := range devices { + if d.Name() == os_path.Components[0] { + return d, nil + } + } + } + + return self.MountFileSystemAccessor.LstatWithOSPath(os_path) +} + +func (self *WindowsNTFSFileSystemAccessor) New( + scope vfilter.Scope) (accessors.FileSystemAccessor, error) { + + // Cache the ntfs accessor for the life of the query. + cache_time := constants.GetNTFSCacheTime(context.Background(), scope) + root_scope := vql_subsystem.GetRootScope(scope) + cached_accessor, ok := vql_subsystem.CacheGet( + root_scope, NTFS_TAG).(*WindowsNTFSFileSystemAccessor) + + // Ignore the filesystem if it is too old - drives may have been + // added or removed. + if ok && cached_accessor.age.Add(cache_time).After(time.Now()) { + return cached_accessor, nil + } + + // Build a virtual filesystem that mounts the various NTFS volumes on it. + root_path, _ := accessors.NewWindowsNTFSPath("") + root_fs := accessors.NewVirtualFilesystemAccessor(root_path) + + result := &WindowsNTFSFileSystemAccessor{ + MountFileSystemAccessor: accessors.NewMountFileSystemAccessor( + root_path, root_fs), + age: time.Now(), + } + + vss, err := Cache.DiscoverVSS() + if err == nil { + for _, fi := range vss { + root_fs.SetVirtualFileInfo(fi) + result.AddMapping( + root_path, // Mount at the root of the filesystem + fi.OSPath(), + NewNTFSFileSystemAccessor( + root_scope, root_path, fi.OSPath(), "file")) + } + } + + logical, err := Cache.DiscoverLogicalDisks() + if err == nil { + for _, fi := range logical { + root_fs.SetVirtualFileInfo(fi) + result.AddMapping( + root_path, + fi.OSPath(), + NewNTFSFileSystemAccessor( + root_scope, root_path, fi.OSPath(), "file")) + } + } + + vql_subsystem.CacheSet(root_scope, NTFS_TAG, result) + return result, nil +} + +func (self WindowsNTFSFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "ntfs", + Description: `Access the NTFS filesystem by parsing NTFS structures.`, + } +} + +func init() { + // For backwards compatibility. It is the same as "ntfs" + accessors.Register(accessors.DescribeAccessor( + &WindowsNTFSFileSystemAccessor{}, + accessors.AccessorDescriptor{ + Name: "lazy_ntfs", + Description: `Access the NTFS filesystem by parsing NTFS structures.`, + })) + accessors.Register(&WindowsNTFSFileSystemAccessor{}) +} diff --git a/vql/windows/filesystems/ntfs_cache.go b/accessors/ntfs/ntfs_cache.go similarity index 90% rename from vql/windows/filesystems/ntfs_cache.go rename to accessors/ntfs/ntfs_cache.go index 9073be7a7..4656ac0ef 100644 --- a/vql/windows/filesystems/ntfs_cache.go +++ b/accessors/ntfs/ntfs_cache.go @@ -1,11 +1,10 @@ -// +build windows - -package filesystems +package ntfs import ( "strings" "time" + "www.velocidex.com/golang/velociraptor/accessors" "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/third_party/cache" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" @@ -83,8 +82,9 @@ func (self *NTFSPathCache) GetDirLRU(dirpath string) (map[string]*CacheMFT, bool return res.(cacheElement).children, true } -func GetNTFSPathCache(scope vfilter.Scope, device string) *NTFSPathCache { - key := "ntfs_path_cache" + device +func GetNTFSPathCache(scope vfilter.Scope, + device *accessors.OSPath, accessor string) *NTFSPathCache { + key := "ntfs_path_cache" + device.String() + accessor // Get the cache context from the root scope's cache cache_ctx, ok := vql_subsystem.CacheGet(scope, key).(*NTFSPathCache) diff --git a/accessors/ntfs/readers/ntfs_reader.go b/accessors/ntfs/readers/ntfs_reader.go new file mode 100644 index 000000000..0927be68d --- /dev/null +++ b/accessors/ntfs/readers/ntfs_reader.go @@ -0,0 +1,301 @@ +package readers + +import ( + "context" + "errors" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "www.velocidex.com/golang/go-ntfs/parser" + ntfs "www.velocidex.com/golang/go-ntfs/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + vql_constants "www.velocidex.com/golang/velociraptor/vql/constants" + "www.velocidex.com/golang/velociraptor/vql/readers" + "www.velocidex.com/golang/vfilter" +) + +var ( + ntfsCacheTotalOpened = promauto.NewCounter(prometheus.CounterOpts{ + Name: "ntfs_cache_total_open", + Help: "Total Number of times we opened the ntfs cache", + }) +) + +// The NTFS parser is responsible for extracting artifacts from +// NTFS. We need to balance two competing needs: + +// 1. We should not read too often and prefer to cache frequently +// accessed sectors in memory as they will be traversed over and +// over (e.g. the $MFT is always looked up). + +// 2. For very long running queries we do not want to cache too long +// or we will be unable to get new data (think event queries). + +// Further we want to destroy the ntfs cache when a query terminates +// so we can free up memory. In practice it is hard to match the +// lifetime of the cache with the lifetime of the scope - the query +// could be quick or take a long time. Closing the underlying file at +// the wrong time can cause issues with the query trying to access it. + +// This reader manages the NTFS Context lifetime in the root scope's +// cache. The idea being that it should be safe to close the +// underlying file at any time. If anyone attempts to access the file, +// the file can be reopened and the ntfs context reparsed on demand. + +// Manage the cache of the NTFS parser - may be shared by multiple +// threads. Contains all the information required to re-open the +// underlying file. +type NTFSCachedContext struct { + mu sync.Mutex + + accessor string + device *accessors.OSPath + + device_is_raw_mft bool + scope vfilter.Scope + paged_reader *readers.AccessorReader + ntfs_ctx *ntfs.NTFSContext + + id uint64 + started time.Time + next_refresh time.Time + + // When this is closed we stop refreshing the cache. Normally + // only closed when the scope is destroyed. + done chan bool +} + +// Close the NTFS context every minute - this forces a refresh and +// reparse of the NTFS device. +func (self *NTFSCachedContext) Start( + ctx context.Context, scope vfilter.Scope) (err error) { + + cache_life := vql_constants.GetNTFSCacheTime(ctx, scope) + + self.mu.Lock() + self.started = utils.GetTime().Now() + self.next_refresh = self.started.Add(cache_life) + self.mu.Unlock() + + lru_size := vql_subsystem.GetIntFromRow( + self.scope, self.scope, constants.NTFS_CACHE_SIZE) + self.paged_reader, err = readers.NewAccessorReader( + self.scope, self.accessor, self.device, int(lru_size)) + + if err != nil { + return err + } + + // Read the header to make sure we can actually read the raw + // device. + if self.device_is_raw_mft { + header := make([]byte, 4) + _, err = self.paged_reader.ReadAt(header, 0) + if err != nil { + return err + } + + if string(header) != "FILE" { + return errors.New("File does not have an MFT Magic") + } + + } else { + header := make([]byte, 8) + _, err = self.paged_reader.ReadAt(header, 3) + if err != nil { + return err + } + + if string(header) != "NTFS " { + return errors.New("No NTFS Magic") + } + } + + go func() { + for { + select { + case <-self.done: + self.mu.Lock() + self.done = nil + self.mu.Unlock() + return + + case <-time.After(cache_life): + self.Close() + } + } + }() + + return err +} + +// Close may be called multiple times and at any time. +func (self *NTFSCachedContext) Close() { + self.mu.Lock() + defer self.mu.Unlock() + + self._CloseWithLock() +} + +func (self *NTFSCachedContext) _CloseWithLock() { + if self.ntfs_ctx != nil { + self.ntfs_ctx.Close() + } + self.paged_reader.Close() +} + +func (self *NTFSCachedContext) detectClusterSize() (int64, int64, error) { + // We need to detect the cluster size or the MFT entry size. We do + // this by checking the signature for the MFT entry. Normally this + // information is given in the boot sector but without the boot + // sector we make do. + buf := make([]byte, 4) + for i := int64(512); i < 8192; i += 512 { + n, err := self.paged_reader.ReadAt(buf, i) + if err != nil || n != 4 { + return 0, 0, err + } + if string(buf) == "FILE" { + return 4096, i, nil + } + } + + return 0, 0, errors.New("Unknown MFT Cluster Size") +} + +func (self *NTFSCachedContext) GetNTFSContext() (ntfs_ctx *ntfs.NTFSContext, err error) { + self.mu.Lock() + defer self.mu.Unlock() + + self.scope.ChargeOp() + + // If the cache is valid just return it. + if self.ntfs_ctx != nil { + return self.ntfs_ctx, nil + } + + if self.device_is_raw_mft { + cluster_size, record_size, err := self.detectClusterSize() + if err != nil { + return nil, err + } + + ntfs_ctx = ntfs.GetNTFSContextFromRawMFT( + self.paged_reader, cluster_size, record_size) + + } else { + ntfs_ctx, err = ntfs.GetNTFSContext(self.paged_reader, 0) + if err != nil { + self._CloseWithLock() + return nil, err + } + } + + ntfs_ctx.SetOptions(GetScopeOptions(self.scope)) + + self.ntfs_ctx = ntfs_ctx + + return self.ntfs_ctx, nil +} + +func GetNTFSContext(scope vfilter.Scope, + device *accessors.OSPath, accessor string) (*ntfs.NTFSContext, error) { + result, err := getNTFSCache(scope, device, accessor, + false /* device_is_raw_mft */) + if err != nil { + return nil, err + } + + return result.GetNTFSContext() +} + +func GetNTFSContextFromRawMFT(scope vfilter.Scope, + mft_filename *accessors.OSPath, accessor string) (*ntfs.NTFSContext, error) { + result, err := getNTFSCache(scope, mft_filename, accessor, + true /* device_is_raw_mft */) + if err != nil { + return nil, err + } + + return result.GetNTFSContext() +} + +func getNTFSCache(scope vfilter.Scope, + device *accessors.OSPath, accessor string, + device_is_raw_mft bool) (*NTFSCachedContext, error) { + key := "ntfsctx_cache" + device.String() + accessor + + // Get the cache context from the root scope's cache + cache_ctx, ok := vql_subsystem.CacheGet(scope, key).(*NTFSCachedContext) + if !ok { + // Create a new cache context. + + cache_ctx = &NTFSCachedContext{ + accessor: accessor, + device: device, + device_is_raw_mft: device_is_raw_mft, + scope: scope, + done: make(chan bool), + id: utils.GetId(), + } + + subctx, cancel := context.WithCancel(context.Background()) + err := cache_ctx.Start(subctx, scope) + if err != nil { + return nil, err + } + + Tracker.Register(cache_ctx) + + // Destroy the context when the scope is done. + err = vql_subsystem.GetRootScope(scope).AddDestructor(func() { + cache_ctx.mu.Lock() + if cache_ctx.done != nil { + close(cache_ctx.done) + } + cache_ctx.mu.Unlock() + cache_ctx.Close() + cancel() + Tracker.Unregister(cache_ctx) + }) + if err != nil { + return nil, err + } + vql_subsystem.CacheSet(scope, key, cache_ctx) + ntfsCacheTotalOpened.Inc() + } + + return cache_ctx, nil +} + +func GetScopeOptions(scope vfilter.Scope) parser.Options { + directory_depth := vql_subsystem.GetIntFromRow( + scope, scope, constants.NTFS_MAX_DIRECTORY_DEPTH) + if directory_depth == 0 { + directory_depth = 20 + } + + max_links := vql_subsystem.GetIntFromRow( + scope, scope, constants.NTFS_MAX_LINKS) + if max_links == 0 { + max_links = 20 + } + + include_short_names := vql_subsystem.GetBoolFromRow( + scope, scope, constants.NTFS_INCLUDE_SHORT_NAMES) + + full_path_resolution := vql_subsystem.GetBoolFromRow( + scope, scope, constants.NTFS_DISABLE_FULL_PATH_RESOLUTION) + + return parser.Options{ + MaxDirectoryDepth: int(directory_depth), + MaxLinks: int(max_links), + IncludeShortNames: include_short_names, + DisableFullPathResolution: full_path_resolution, + } +} diff --git a/accessors/ntfs/readers/tracker.go b/accessors/ntfs/readers/tracker.go new file mode 100644 index 000000000..1f30de7ce --- /dev/null +++ b/accessors/ntfs/readers/tracker.go @@ -0,0 +1,102 @@ +package readers + +import ( + "context" + "sync" + "time" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/services/debug" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +var ( + Tracker = &NTFSCacheTracker{ + current_contexts: make(map[uint64]*NTFSCachedContext), + } +) + +type NTFSCacheTracker struct { + mu sync.Mutex + current_contexts map[uint64]*NTFSCachedContext +} + +func (self *NTFSCacheTracker) Register(context *NTFSCachedContext) { + self.mu.Lock() + defer self.mu.Unlock() + + self.current_contexts[context.id] = context +} + +func (self *NTFSCacheTracker) Unregister(context *NTFSCachedContext) { + self.mu.Lock() + defer self.mu.Unlock() + + delete(self.current_contexts, context.id) +} + +func (self *NTFSCacheTracker) ProfileWriter(ctx context.Context, + scope vfilter.Scope, output_chan chan vfilter.Row) { + self.mu.Lock() + defer self.mu.Unlock() + + for _, ref := range self.current_contexts { + ref.ProfileWriter(ctx, scope, output_chan) + } +} + +func (self *NTFSCachedContext) ProfileWriter(ctx context.Context, + scope vfilter.Scope, output_chan chan vfilter.Row) { + + self.mu.Lock() + defer self.mu.Unlock() + + next_refresh := "" + if !self.next_refresh.IsZero() { + next_refresh = self.next_refresh.Sub(utils.GetTime().Now()). + Round(time.Second).String() + } + + var mft_entries, cached_pages *ordereddict.Dict + if self.paged_reader != nil { + cached_pages = self.paged_reader.Stats() + } + + if self.ntfs_ctx != nil { + mft_entries = self.ntfs_ctx.Stats() + } + + started := "" + if !self.started.IsZero() { + started = utils.GetTime().Now().Sub(self.started). + Round(time.Second).String() + } + + select { + case <-ctx.Done(): + return + + case output_chan <- ordereddict.NewDict(). + Set("ID", self.id). + Set("Active", self.ntfs_ctx != nil). + Set("Accessor", self.accessor). + Set("Device", self.device). + Set("Started", started). + + // Next time we reset the NTFS cache. + Set("NextRefresh", next_refresh). + Set("PageCache", cached_pages). + Set("MFTEntries", mft_entries): + } + +} + +func init() { + debug.RegisterProfileWriter(debug.ProfileWriterInfo{ + Name: "NTFS Cache Tracker", + Description: "Track NTFS caches", + ProfileWriter: Tracker.ProfileWriter, + Categories: []string{"Global", "VQL", "Plugins"}, + }) +} diff --git a/accessors/ntfs/vss.go b/accessors/ntfs/vss.go new file mode 100644 index 000000000..30e6e6267 --- /dev/null +++ b/accessors/ntfs/vss.go @@ -0,0 +1,227 @@ +//go:build windows +// +build windows + +package ntfs + +import ( + "context" + "strings" + "time" + + "www.velocidex.com/golang/velociraptor/accessors" + vconstants "www.velocidex.com/golang/velociraptor/constants" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/constants" + "www.velocidex.com/golang/vfilter" +) + +const ( + VSS_TAG = "$__NTFS_VSS_Accessor" +) + +type WindowsVSSFileSystemAccessor struct { + *WindowsNTFSFileSystemAccessor + + // A list of the roots we are interested in. + vss_roots []*accessors.OSPath +} + +func (self WindowsVSSFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + // Cache the ntfs accessor for the life of the query. + cache_time := constants.GetNTFSCacheTime(context.Background(), scope) + root_scope := vql_subsystem.GetRootScope(scope) + cached_accessor, ok := vql_subsystem.CacheGet( + root_scope, VSS_TAG).(*WindowsVSSFileSystemAccessor) + + // Ignore the filesystem if it is too old - drives may have been + // added or removed. + if ok && cached_accessor.age.Add(cache_time).After(time.Now()) { + return cached_accessor, nil + } + + // TODO: Add mechanism for the user to restrict VSS range by time + // of interest. + base_fs_any, err := self.WindowsNTFSFileSystemAccessor.New(scope) + if err != nil { + return nil, err + } + + base_fs := base_fs_any.(*WindowsNTFSFileSystemAccessor) + + result := &WindowsVSSFileSystemAccessor{ + WindowsNTFSFileSystemAccessor: base_fs, + } + + roots, err := result.getVSSRoots(scope) + if err != nil { + return nil, err + } + result.vss_roots = roots + + vql_subsystem.CacheSet(root_scope, VSS_TAG, result) + return result, nil +} + +func (self *WindowsVSSFileSystemAccessor) getVSSRoots( + scope vfilter.Scope) ([]*accessors.OSPath, error) { + + var result []*accessors.OSPath + + max_age := vql_subsystem.GetFloatFromRow( + scope, scope, vconstants.VSS_MAX_AGE_DAYS) + + root_path, _ := accessors.NewWindowsNTFSPath("") + roots, err := self.WindowsNTFSFileSystemAccessor.ReadDirWithOSPath(root_path) + if err != nil { + return nil, err + } + + for _, r := range roots { + device, _ := r.Data().GetString("DeviceObject") + if strings.Contains(device, "HarddiskVolumeShadowCopy") { + install_date, _ := r.Data().GetString("InstallDate") + if len(install_date) < 14 { + continue + } + + parsed, err := time.Parse("20060102150405", install_date[:14]) + if err != nil { + scope.Log("ERROR: Unable to parse time %v", err) + continue + } + + if max_age > 0 { + // Age is too long ago skip it + if time.Now().Sub(parsed).Seconds() > max_age*24*60*60 { + continue + } + scope.Log("vss: Found VSS %v created %v within Max Age of %v days\n", + device, parsed.UTC().Format(time.RFC3339), max_age) + } else { + scope.Log("vss: Found VSS %v that was created at %v\n", + device, parsed.UTC().Format(time.RFC3339)) + } + + result = append(result, r.OSPath()) + } + } + + return result, nil + +} + +// Merge the results from all shadows into a single list. +func (self *WindowsVSSFileSystemAccessor) ReadDirWithOSPath( + fullpath *accessors.OSPath) (res []accessors.FileInfo, err error) { + + root_list, err := self.WindowsNTFSFileSystemAccessor.ReadDirWithOSPath(fullpath) + if err != nil { + return nil, err + } + + if len(fullpath.Components) == 0 { + return root_list, nil + } + + by_mft_id := make(map[string][]accessors.FileInfo) + for _, i := range root_list { + mft_id, pres := i.Data().GetString("mft") + if pres { + existing, _ := by_mft_id[mft_id] + existing = append(existing, VSSFileInfo{ + FileInfo: i, + device: fullpath.Components[0], + }) + by_mft_id[mft_id] = existing + } + } + + // Now list each of the VSS and merge with the by_mft_id list. + relative_path := fullpath.Components[1:] + for _, root := range self.vss_roots { + path := root.Append(relative_path...) + + files, err := self.WindowsNTFSFileSystemAccessor.ReadDirWithOSPath(path) + if err != nil { + // Path may not exist in this shadow. + continue + } + + for _, i := range files { + mft_id, pres := i.Data().GetString("mft") + if pres { + existing, _ := by_mft_id[mft_id] + if !self.file_found(existing, i) { + existing = append(existing, VSSFileInfo{ + FileInfo: i, + device: root.Components[0], + }) + } + by_mft_id[mft_id] = existing + } + } + } + + result := make([]accessors.FileInfo, 0, len(by_mft_id)) + for _, v := range by_mft_id { + for _, item := range v { + result = append(result, item) + } + } + + return result, err +} + +// Search for the file needle in the file haystack +func (self *WindowsVSSFileSystemAccessor) file_found( + haystack []accessors.FileInfo, needle accessors.FileInfo) bool { + + name := needle.Name() + mtime := needle.Mtime().UnixNano() + size := needle.Size() + is_dir := needle.IsDir() + + for _, old := range haystack { + // For directories we need to only return one version because + // glob will descend it in any case (regardless of version). + if is_dir && old.Name() == name { + return true + } + + if mtime == old.Mtime().UnixNano() && size == old.Size() { + return true + } + } + + return false +} + +type VSSFileInfo struct { + accessors.FileInfo + device string +} + +// Needed for glob - present a unique name for the file for +// deduplication. +func (self VSSFileInfo) UniqueName() string { + u, ok := self.FileInfo.(accessors.UniqueBasename) + if ok { + return self.device + u.UniqueName() + } + return self.device + self.FileInfo.Name() +} + +func (self WindowsVSSFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "ntfs_vss", + Description: `Access the NTFS filesystem by considering all VSS.`, + } +} + +func init() { + accessors.Register(&WindowsVSSFileSystemAccessor{ + WindowsNTFSFileSystemAccessor: &WindowsNTFSFileSystemAccessor{}, + }) +} diff --git a/accessors/offset/offset.go b/accessors/offset/offset.go new file mode 100644 index 000000000..04a6c7839 --- /dev/null +++ b/accessors/offset/offset.go @@ -0,0 +1,147 @@ +// An accessor that maps ranges from a delegate. + +package offset + +import ( + "fmt" + "io" + "os" + "strconv" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/zip" + "www.velocidex.com/golang/vfilter" +) + +type OffsetFileInfo struct { + accessors.FileInfo + + _full_path *accessors.OSPath +} + +func (self *OffsetFileInfo) OSPath() *accessors.OSPath { + return self._full_path +} + +type OffsetReader struct { + reader io.ReadSeekCloser + info accessors.FileInfo + + // Current offset of the reader in delegate coordinates + offset int64 + + // Constant offset we add the the delegate reader. + base_offset int64 + + // The OSPath object that is required to access this + // file. (includes delegate and offset). + _full_path *accessors.OSPath +} + +func (self *OffsetReader) Close() error { + return self.reader.Close() +} + +func (self *OffsetReader) Read(buff []byte) (int, error) { + new_pos, err := self.reader.Seek(self.offset, os.SEEK_SET) + if err != nil { + return int(new_pos), err + } + + n, err := self.reader.Read(buff) + self.offset += int64(n) + return n, err +} + +func (self *OffsetReader) Seek(offset int64, whence int) (int64, error) { + + // Callers are operating in the offsetted coordinate system so the + // real offset should be the offset they asked for plus the base + // offset. + if whence == os.SEEK_SET { + offset += self.base_offset + } + + new_delegate_offset, err := self.reader.Seek(offset, whence) + if err != nil { + return new_delegate_offset, err + } + + // Remember the delegate offset + self.offset = new_delegate_offset + + // Report the new offset in terms of the offsetted coordinate. + return self.offset - self.base_offset, nil +} + +func (self *OffsetReader) LStat() (accessors.FileInfo, error) { + return &OffsetFileInfo{ + FileInfo: self.info, + _full_path: self._full_path, + }, nil +} + +func GetOffsetFile(full_path *accessors.OSPath, scope vfilter.Scope) ( + zip.ReaderStat, error) { + + if len(full_path.Components) == 0 { + return nil, fmt.Errorf("Offset accessor expects an offset at root path") + + } + + offset, err := strconv.ParseInt(full_path.Components[0], 0, 64) + if err != nil { + return nil, fmt.Errorf("Offset accessor expects an offset path: %w", err) + } + + pathspec := full_path.PathSpec() + + // The gzip accessor must use a delegate but if one is not + // provided we use the "auto" accessor, to open the underlying + // file. + if pathspec.DelegateAccessor == "" && pathspec.GetDelegatePath() == "" { + pathspec.DelegatePath = pathspec.Path + pathspec.DelegateAccessor = "auto" + } + + accessor, err := accessors.GetAccessor(pathspec.DelegateAccessor, scope) + if err != nil { + scope.Log("%v: did you provide a URL or PathSpec?", err) + return nil, err + } + + delegate_path := pathspec.GetDelegatePath() + fd, err := accessor.Open(delegate_path) + if err != nil { + return nil, err + } + + stat, err := accessor.Lstat(delegate_path) + if err != nil { + // If we can not call stat on the file it is not a fatal + // error. For example, raw files are not always statable - in + // that case we provide a fake stat object. + stat = &accessors.VirtualFileInfo{ + Path: full_path, + Size_: 1<<63 - 1, + } + } + + return &OffsetReader{ + reader: fd, + info: stat, + offset: offset, + base_offset: offset, + _full_path: full_path, + }, nil +} + +func init() { + accessors.Register(accessors.DescribeAccessor( + zip.NewGzipFileSystemAccessor( + accessors.MustNewLinuxOSPath(""), GetOffsetFile), + accessors.AccessorDescriptor{ + Name: "offset", + Description: `Allow reading another file from a specific offset.`, + })) +} diff --git a/accessors/offset/offset_test.go b/accessors/offset/offset_test.go new file mode 100644 index 000000000..2eae68953 --- /dev/null +++ b/accessors/offset/offset_test.go @@ -0,0 +1,43 @@ +package offset + +import ( + "io/ioutil" + "os" + "testing" + + "www.velocidex.com/golang/velociraptor/accessors" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + + _ "www.velocidex.com/golang/velociraptor/accessors/data" +) + +func TestAccessorOffset(t *testing.T) { + scope := vql_subsystem.MakeScope() + accessor, err := accessors.GetAccessor("offset", scope) + assert.NoError(t, err) + + // The Path is really a json encoded sparse map. + pathspec := &accessors.PathSpec{ + DelegateAccessor: "data", + DelegatePath: "This is a bit of text", + Path: `10`, + } + + fd, err := accessor.Open(pathspec.String()) + assert.NoError(t, err) + + data, err := ioutil.ReadAll(fd) + assert.NoError(t, err) + + assert.Equal(t, "bit of text", string(data)) + + // Check that Seeking works + n, err := fd.Seek(3, os.SEEK_SET) + assert.NoError(t, err) + assert.Equal(t, int64(3), n) + + m, err := fd.Read(data) + assert.NoError(t, err) + assert.Equal(t, " of text", string(data[:m])) +} diff --git a/accessors/overlay/fixtures/TestOverlay.golden b/accessors/overlay/fixtures/TestOverlay.golden new file mode 100644 index 000000000..c7c15d783 --- /dev/null +++ b/accessors/overlay/fixtures/TestOverlay.golden @@ -0,0 +1,8 @@ +{ + "file1.txt": "Hello", + "file1.txt Stat": "file1.txt", + "file2.txt": "Hello Two", + "file2.txt Stat": "file2.txt", + "subdir:file2.txt": "Hello Subdir", + "subdir:file2.txt Stat": "subdir:file2.txt" +} \ No newline at end of file diff --git a/accessors/overlay/overlay.go b/accessors/overlay/overlay.go new file mode 100644 index 000000000..283658317 --- /dev/null +++ b/accessors/overlay/overlay.go @@ -0,0 +1,206 @@ +/* The overlay accessor merges a number of other paths */ + +package overlay + +import ( + "context" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/arg_parser" + "www.velocidex.com/golang/vfilter/utils/dict" +) + +type OverlayFileSystemAccessorArgs struct { + Paths []*accessors.OSPath `vfilter:"required,field=paths,doc=A list of paths to try to resolve."` + Accessor string `vfilter:"optional,field=accessor,doc=File accessor"` +} + +type OverlayFileSystemAccessor struct { + ctx context.Context + scope vfilter.Scope +} + +func (self OverlayFileSystemAccessor) ParsePath(path string) (*accessors.OSPath, error) { + return accessors.NewLinuxOSPath(path) +} + +func (self OverlayFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + result := &OverlayFileSystemAccessor{ + ctx: context.TODO(), + scope: scope, + } + return result, nil +} + +func (self OverlayFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "overlay", + Description: `Merges several paths into a single path.`, + // Permissions are actually enforced by the delegated accessors + Permissions: []acls.ACL_PERMISSION{}, + ScopeVar: constants.OVERLAY_ACCESSOR_DELEGATES, + ArgType: &OverlayFileSystemAccessorArgs{}, + } +} + +func (self OverlayFileSystemAccessor) ReadDir( + path string) ([]accessors.FileInfo, error) { + + parsed_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(parsed_path) +} + +func (self OverlayFileSystemAccessor) ReadDirWithOSPath( + path *accessors.OSPath) (res []accessors.FileInfo, err error) { + + overlayer, err := GetOverlayConfig(self.ctx, self.scope) + if err != nil { + return nil, err + } + + accessor, err := accessors.GetAccessor(overlayer.Accessor, self.scope) + if err != nil { + return nil, err + } + + seen := make(map[string]bool) + + for _, basepath := range overlayer.Paths { + delegate_path := basepath.Append(path.Components...) + delegate_dir, err := accessor.ReadDirWithOSPath(delegate_path) + if err != nil { + continue + } + + base := basepath.TrimComponents(basepath.Components...) + + for _, fsinfo := range delegate_dir { + name := fsinfo.Name() + _, pres := seen[name] + if pres { + continue + } + + seen[name] = true + + item := accessors.NewFileInfoWrapper( + fsinfo, base, basepath.Copy()) + res = append(res, item) + } + } + + return res, nil +} + +func (self OverlayFileSystemAccessor) OpenWithOSPath( + path *accessors.OSPath) (res accessors.ReadSeekCloser, res_err error) { + + overlayer, err := GetOverlayConfig(self.ctx, self.scope) + if err != nil { + return nil, err + } + + accessor, err := accessors.GetAccessor(overlayer.Accessor, self.scope) + if err != nil { + return nil, err + } + + for _, basepath := range overlayer.Paths { + res, res_err = accessor.OpenWithOSPath( + basepath.Append(path.Components...)) + // Return the first successful opened file + if res_err == nil { + break + } + } + + if res_err == nil && res == nil { + return nil, utils.NotFoundError + } + return res, res_err +} + +func (self OverlayFileSystemAccessor) Open( + filename string) (accessors.ReadSeekCloser, error) { + + parsed_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(parsed_path) +} + +func (self OverlayFileSystemAccessor) Lstat(path string) (accessors.FileInfo, error) { + + parsed_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(parsed_path) +} + +func (self OverlayFileSystemAccessor) LstatWithOSPath( + path *accessors.OSPath) (res accessors.FileInfo, res_err error) { + + overlayer, err := GetOverlayConfig(self.ctx, self.scope) + if err != nil { + return nil, err + } + + accessor, err := accessors.GetAccessor(overlayer.Accessor, self.scope) + if err != nil { + return nil, err + } + + for _, basepath := range overlayer.Paths { + res, res_err = accessor.LstatWithOSPath( + basepath.Append(path.Components...)) + // Return the first successful opened file + if res_err == nil { + base := basepath.TrimComponents(basepath.Components...) + res = accessors.NewFileInfoWrapper(res, base, basepath.Copy()) + break + } + } + + if res_err == nil && res == nil { + return nil, utils.NotFoundError + } + return res, res_err +} + +func init() { + accessors.Register(&OverlayFileSystemAccessor{}) +} + +func GetOverlayConfig( + ctx context.Context, + scope vfilter.Scope) (res *OverlayFileSystemAccessorArgs, err error) { + + setting, pres := scope.Resolve(constants.OVERLAY_ACCESSOR_DELEGATES) + if !pres { + setting = ordereddict.NewDict() + } + + args := dict.RowToDict(ctx, scope, setting) + arg := &OverlayFileSystemAccessorArgs{} + err = arg_parser.ExtractArgsWithContext(ctx, scope, args, arg) + if err != nil { + return nil, err + } + + return arg, nil +} diff --git a/accessors/overlay/overlay_test.go b/accessors/overlay/overlay_test.go new file mode 100644 index 000000000..687c6fe45 --- /dev/null +++ b/accessors/overlay/overlay_test.go @@ -0,0 +1,114 @@ +package overlay + +import ( + "log" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/Velocidex/ordereddict" + "github.com/stretchr/testify/suite" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/utils/tempfile" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" + + _ "www.velocidex.com/golang/velociraptor/accessors/file" +) + +type OverlayAccessorTestSuite struct { + suite.Suite + tmpdir string +} + +func (self *OverlayAccessorTestSuite) SetupTest() { + tmpdir, err := tempfile.TempDir("accessor_test") + assert.NoError(self.T(), err) + + self.tmpdir = strings.ReplaceAll(tmpdir, "\\", "/") +} + +func (self *OverlayAccessorTestSuite) TearDownTest() { + os.RemoveAll(self.tmpdir) // clean up +} + +func (self *OverlayAccessorTestSuite) makeFile(path string, content string) { + path = strings.TrimLeft(path, "/") + + file_path := filepath.Join(self.tmpdir, path) + os.MkdirAll(filepath.Dir(file_path), 0700) + + fd, err := os.OpenFile(file_path, + os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) + assert.NoError(self.T(), err) + fd.Write([]byte(content)) + fd.Close() +} + +func (self *OverlayAccessorTestSuite) TestOverlay() { + self.makeFile("foo1/file1.txt", "Hello") + self.makeFile("foo2/file2.txt", "Hello Two") + self.makeFile("foo2/subdir/file2.txt", "Hello Subdir") + + scope := vql_subsystem.MakeScope(). + AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, + acl_managers.NullACLManager{}). + Set(constants.OVERLAY_ACCESSOR_DELEGATES, + ordereddict.NewDict(). + Set("accessor", "file"). + Set("paths", []string{ + self.tmpdir + "/foo1", + self.tmpdir + "/foo2", + }))) + + scope.SetLogger(log.New(os.Stderr, " ", 0)) + accessor, err := accessors.GetAccessor("overlay", scope) + assert.NoError(self.T(), err) + + golden := ordereddict.NewDict() + + check_dir := func(file_path string) { + files, err := accessor.ReadDir(file_path) + assert.NoError(self.T(), err) + + for _, f := range files { + if f.IsDir() { + continue + } + + fd, err := accessor.OpenWithOSPath(f.OSPath()) + assert.NoError(self.T(), err) + + data, err := utils.ReadAllWithLimit(fd, constants.MAX_MEMORY) + assert.NoError(self.T(), err) + fd.Close() + + components := strings.Join(f.OSPath().Components, ":") + + golden.Set(components, string(data)) + + stat, err := accessor.LstatWithOSPath(f.OSPath()) + assert.NoError(self.T(), err) + + golden.Set(components+" Stat", + strings.Join(stat.OSPath().Components, ":")) + } + } + check_dir("/") + check_dir("/subdir/") + + goldie.Assert(self.T(), "TestOverlay", json.MustMarshalIndent(golden)) + +} + +// Test both the Windows and Linux File accessor. +func TestOverlayAccessor(t *testing.T) { + suite.Run(t, &OverlayAccessorTestSuite{}) +} diff --git a/glob/pathspec.go b/accessors/pathspec.go similarity index 82% rename from glob/pathspec.go rename to accessors/pathspec.go index 2fcbe8082..892f6e4f6 100644 --- a/glob/pathspec.go +++ b/accessors/pathspec.go @@ -1,9 +1,9 @@ -package glob +package accessors import ( "net/url" - errors "github.com/pkg/errors" + errors "github.com/go-errors/errors" "www.velocidex.com/golang/velociraptor/json" ) @@ -75,23 +75,45 @@ var ( deprecated but still supported - it will eventually be dropped. */ type PathSpec struct { - DelegateAccessor string `json:"DelegateAccessor,omitempty"` - DelegatePath string `json:"DelegatePath,omitempty"` - Delegate *PathSpec `json:"Delegate,omitempty"` - Path string `json:"Path,omitempty"` + DelegateAccessor string `json:"DelegateAccessor,omitempty"` + DelegatePath string `json:"DelegatePath,omitempty"` + + // This standard for DelegatePath above and allows a more + // convenient way to pass recursive pathspecs down. + Delegate *PathSpec `json:"Delegate,omitempty"` + Path string `json:"Path,omitempty"` // Keep track of if the pathspec came from a URL based for // backwards compatibility. url_based bool } +func (self PathSpec) Copy() *PathSpec { + result := self + if result.Delegate != nil { + result.Delegate = result.Delegate.Copy() + } + + return &result +} + func (self PathSpec) GetDelegatePath() string { + // We allow the delegate path to be encoded as a nested pathspec + // for covenience. if self.Delegate != nil { return self.Delegate.String() } return self.DelegatePath } +func (self PathSpec) GetDelegateAccessor() string { + return self.DelegateAccessor +} + +func (self PathSpec) GetPath() string { + return self.Path +} + func (self PathSpec) String() string { if self.url_based { result := url.URL{ @@ -108,7 +130,7 @@ func (self PathSpec) String() string { func PathSpecFromString(parsed string) (*PathSpec, error) { if len(parsed) == 0 { - return nil, InvalidPathSpec + return &PathSpec{}, nil } // It is a serialized JSON object. diff --git a/accessors/pipe/pipe.go b/accessors/pipe/pipe.go new file mode 100644 index 000000000..b3887f88f --- /dev/null +++ b/accessors/pipe/pipe.go @@ -0,0 +1,218 @@ +package pipe + +import ( + "context" + "errors" + "fmt" + "io" + "os" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/arg_parser" + "www.velocidex.com/golang/vfilter/types" +) + +// A pipe is a VQL constract that emulates a file from a query. +// +// For example: +// LET MyPipe = Pipe(query={ +// SELECT _value FROM range(start=0, end=10, step=1) +// }, sep="\n") +// LET read_file(filename="MyPipe", accessor="pipe") AS Data FROM scope() +// +// Data = "1\n2\n3\n4\n + +type Pipe struct { + output_chan <-chan vfilter.Row + ctx context.Context + sep []byte +} + +func (self *Pipe) Close() error { return nil } +func (self *Pipe) Seek(offset int64, whence int) (int64, error) { + return 0, errors.New("Not Seekable") +} + +func (self *Pipe) IsSeekable() bool { + return false +} + +func (self *Pipe) Stat() (os.FileInfo, error) { + return nil, errors.New("Not implemented") +} + +func (self *Pipe) Read(buff []byte) (int, error) { + select { + + case <-self.ctx.Done(): + return 0, io.EOF + + case row, ok := <-self.output_chan: + if !ok { + return 0, io.EOF + } + + switch t := row.(type) { + case *ordereddict.Dict: + for _, v := range t.Values() { + switch t := v.(type) { + case string: + out := append([]byte(t), self.sep...) + return utils.MemCpy(buff, out), nil + + case []byte: + return utils.MemCpy(buff, + append(t, self.sep...)), nil + + default: + data := fmt.Sprintf("%v", v) + out := append([]byte(data), self.sep...) + return utils.MemCpy(buff, out), nil + } + } + } + + data := fmt.Sprintf("%v", row) + out := append([]byte(data), self.sep...) + return utils.MemCpy(buff, out), nil + } +} + +type PipeFunctionArgs struct { + Name string `vfilter:"optional,field=name,doc=Name to call the pipe"` + Query types.StoredQuery `vfilter:"optional,field=query,doc=Run this query to generator data - the first column will be appended to pipe data."` + Sep string `vfilter:"optional,field=sep,doc=The separator that will be used to split each read (default: no separator will be used)"` +} + +type PipeFunction struct{} + +func (self *PipeFunction) Call(ctx context.Context, + scope vfilter.Scope, + args *ordereddict.Dict) vfilter.Any { + arg := &PipeFunctionArgs{} + err := arg_parser.ExtractArgsWithContext(ctx, scope, args, arg) + if err != nil { + scope.Log("pipe: %s", err.Error()) + return false + } + + if arg.Name == "" { + arg.Name = vfilter.FormatToString(scope, arg.Query) + } + + key := "pipe:" + arg.Name + cached_pipe_any := vql_subsystem.CacheGet(scope, key) + cached_pipe, ok := cached_pipe_any.(*Pipe) + + defer vql_subsystem.CacheSet(scope, key, cached_pipe) + + if !ok || utils.IsNil(cached_pipe) { + row_chan := arg.Query.Eval(ctx, scope) + cached_pipe = &Pipe{ + output_chan: row_chan, + ctx: ctx, + sep: []byte(arg.Sep), + } + } + + return cached_pipe +} + +func (self PipeFunction) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.FunctionInfo { + return &vfilter.FunctionInfo{ + Name: "pipe", + Doc: "A pipe allows plugins that use files to read data from a vql query.", + ArgType: type_map.AddType(scope, &PipeFunctionArgs{}), + } +} + +type PipeFilesystemAccessor struct { + scope vfilter.Scope +} + +func (self PipeFilesystemAccessor) ParsePath(path string) (*accessors.OSPath, error) { + return accessors.NewLinuxOSPath(path) +} + +func (self PipeFilesystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "pipe", + Description: `Read from a VQL pipe.`, + } +} + +func (self PipeFilesystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + return PipeFilesystemAccessor{scope}, nil +} + +func (self PipeFilesystemAccessor) Lstat(filename string) ( + accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +func (self PipeFilesystemAccessor) LstatWithOSPath(full_path *accessors.OSPath) ( + accessors.FileInfo, error) { + + return &accessors.VirtualFileInfo{ + Path: full_path, + }, nil +} + +func (self PipeFilesystemAccessor) ReadDir(path string) ( + []accessors.FileInfo, error) { + return nil, errors.New("Not implemented") +} + +func (self PipeFilesystemAccessor) ReadDirWithOSPath(path *accessors.OSPath) ( + []accessors.FileInfo, error) { + return nil, errors.New("Not implemented") +} + +// The path is the name of the scope variable that holds the pipe object +func (self PipeFilesystemAccessor) Open(variable string) (accessors.ReadSeekCloser, error) { + variable_data, pres := self.scope.Resolve(variable) + if !pres || utils.IsNil(variable_data) { + return nil, utils.NotFoundError + } + variable_data_lazy, ok := variable_data.(types.StoredExpression) + if ok { + ctx, cancel := context.WithCancel(context.Background()) + err := vql_subsystem.GetRootScope(self.scope).AddDestructor(cancel) + if err != nil { + cancel() + return nil, err + } + + variable_data = variable_data_lazy.Reduce(ctx, self.scope) + } + + pipe, ok := variable_data.(*Pipe) + if !ok { + return nil, utils.NotFoundError + } + + return pipe, nil +} + +func (self PipeFilesystemAccessor) OpenWithOSPath( + path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + if len(path.Components) != 1 { + return nil, utils.NotFoundError + } + return self.Open(path.Components[0]) +} + +func init() { + accessors.Register(&PipeFilesystemAccessor{}) + vql_subsystem.RegisterFunction(&PipeFunction{}) +} diff --git a/accessors/process/doc.go b/accessors/process/doc.go new file mode 100644 index 000000000..f2ef9d286 --- /dev/null +++ b/accessors/process/doc.go @@ -0,0 +1 @@ +package process diff --git a/accessors/process/process_address_space.go b/accessors/process/process_address_space.go new file mode 100644 index 000000000..92512a033 --- /dev/null +++ b/accessors/process/process_address_space.go @@ -0,0 +1,231 @@ +//go:build linux || darwin +// +build linux darwin + +// An accessor for process address space. +// Using this accessor it is possible to read directly from different processes, e.g. +// read_file(filename="/434", accessor="process") + +package process + +import ( + "errors" + "fmt" + "io" + "os" + "sync" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/uploads" + "www.velocidex.com/golang/vfilter" +) + +const PAGE_SIZE = 0x1000 + +type ReadAtCloser interface { + io.ReaderAt + io.Closer +} + +type ProcessReader struct { + mu sync.Mutex + pid uint64 + offset int64 + size int64 + + // A file handle to the /proc/pid/mem file. + handle ReadAtCloser + ranges []*uploads.Range +} + +func (self *ProcessReader) Close() error { + return self.handle.Close() +} + +func (self *ProcessReader) Ranges() []uploads.Range { + self.mu.Lock() + defer self.mu.Unlock() + + result := []uploads.Range{} + size := int64(0) + for _, rng := range self.ranges { + // Fill in a sparse range if needed + if rng.Offset > size { + result = append(result, uploads.Range{ + Offset: size, + Length: rng.Offset - size, + IsSparse: true, + }) + } + + // Move the pointer past the end of this range. + size = rng.Offset + rng.Length + + // Add a real data run + result = append(result, *rng) + } + return result +} + +// Repeat the read operation one page at the time in order to retrieve +// as much data as possible. +func (self *ProcessReader) readDistinctPages(buf []byte) (int, error) { + page_count := len(buf) / PAGE_SIZE + if page_count <= 1 { + return page_count * PAGE_SIZE, nil + } + + // Read as many pages as possible into the buffer ignoring errors. + for i := 0; i < page_count; i += 1 { + buf_start := i * PAGE_SIZE + buf_end := buf_start + PAGE_SIZE + + // Repeat the read with a single page at the time. + _, err := self.handle.ReadAt(buf[buf_start:buf_end], self.offset) + if err != nil { + // Error occured reading a single page, zero + // it out and skip the page. + for i := buf_start; i < buf_end; i++ { + buf[i] = 0 + } + self.offset += PAGE_SIZE + } + } + + return page_count * PAGE_SIZE, nil +} + +func (self *ProcessReader) Read(buf []byte) (int, error) { + self.mu.Lock() + defer self.mu.Unlock() + + current_range, next_range := uploads.GetNextRange(self.offset, self.ranges) + // Current offset is inside the range. + if current_range != nil { + to_read := current_range.Offset + current_range.Length - self.offset + if to_read > int64(len(buf)) { + to_read = int64(len(buf)) + } + + // Read memory from process at specified offset. + _, err := self.handle.ReadAt(buf[:to_read], self.offset) + + // A read error occured - split the read into multiple page + // size reads to get as much data as we can out of the + // region. Note: We always return as much data as was + // required, we simply null pad the missing data. Therefore if + // a reader askes to read from a memory region that contains + // no data, we never return an error - just zero pad those + // regions. + if err != nil { + return self.readDistinctPages(buf) + } + + // Advance the read pointer. + self.offset += to_read + + return int(to_read), nil + } + + // The current offset is not inside any range so we null pad until + // the next range. + if next_range != nil { + to_read := next_range.Offset - self.offset + if to_read > int64(len(buf)) { + to_read = int64(len(buf)) + } + + // Clear the buffer + for i := range buf[:to_read] { + buf[i] = 0 + } + self.offset += to_read + return int(to_read), nil + } + + // Range is past the end of file + return 0, io.EOF +} + +func (self *ProcessReader) Seek(offset int64, whence int) (int64, error) { + self.mu.Lock() + defer self.mu.Unlock() + + switch whence { + case 0: + self.offset = offset + case 1: + self.offset += offset + case 2: + self.offset = self.size + } + + return int64(self.offset), nil +} + +func (self *ProcessReader) Stat() (os.FileInfo, error) { + full_path, _ := accessors.NewLinuxOSPath(fmt.Sprintf("%v", self.pid)) + return &accessors.VirtualFileInfo{ + Path: full_path, + Size_: self.size, + }, nil +} + +type ProcessAccessor struct{} + +func (self ProcessAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "process", + Description: `Access process memory like a file. The Path is taken in the form "/", i.e. the pid appears as the top level file.`, + Permissions: []acls.ACL_PERMISSION{acls.MACHINE_STATE}, + } +} + +func (self ProcessAccessor) New(scope vfilter.Scope) (accessors.FileSystemAccessor, error) { + return &ProcessAccessor{}, nil +} + +func (self ProcessAccessor) ReadDir(path string) ([]accessors.FileInfo, error) { + return nil, errors.New("Unable to list all processes, use the pslist() plugin") +} + +func (self ProcessAccessor) ReadDirWithOSPath( + path *accessors.OSPath) ([]accessors.FileInfo, error) { + return nil, errors.New("Unable to list all processes, use the pslist() plugin") +} + +func (self ProcessAccessor) Lstat(filename string) (accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return &accessors.VirtualFileInfo{ + Path: full_path, + }, nil +} + +func (self ProcessAccessor) LstatWithOSPath( + full_path *accessors.OSPath) (accessors.FileInfo, error) { + return &accessors.VirtualFileInfo{ + Path: full_path, + }, nil +} + +func (self ProcessAccessor) ParsePath(path string) (*accessors.OSPath, error) { + return accessors.NewLinuxOSPath(path) +} + +func (self *ProcessAccessor) Open( + filename string) (accessors.ReadSeekCloser, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func init() { + accessors.Register(&ProcessAccessor{}) +} diff --git a/accessors/process/process_address_space_darwin.go b/accessors/process/process_address_space_darwin.go new file mode 100644 index 000000000..9919d4bff --- /dev/null +++ b/accessors/process/process_address_space_darwin.go @@ -0,0 +1,131 @@ +//go:build darwin && cgo +// +build darwin,cgo + +// An accessor for process address space. +// Using this accessor it is possible to read directly from different processes, e.g. +// read_file(filename="/434", accessor="process") + +package process + +import ( + "errors" + "fmt" + "io" + "strconv" + "unsafe" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/uploads" +) + +/* + +#include +#include +#include +#include +#include + +mach_port_t get_task_self () { + return mach_task_self(); +} + +// Override go type checking - xnu converts void * to vm_address_t (ulong) +// which is unsafe on 32 bit platforms. +kern_return_t +_vm_read_overwrite( + vm_map_t map, + vm_address_t address, + vm_size_t size, + char *data, + vm_size_t *data_size) { + return vm_read_overwrite(map, address, size, (vm_address_t)(data), data_size); +}; + +*/ +import "C" + +const ( + // https://opensource.apple.com/source/xnu/xnu-792/osfmk/mach/vm_region.h.auto.html + VM_REGION_BASIC_INFO = 10 + VM_REGION_BASIC_INFO_COUNT_64 = 9 +) + +type darwinProcessReader struct { + task C.task_t +} + +func (self darwinProcessReader) ReadAt(buff []byte, offset int64) (int, error) { + var size C.ulong + + kr := C._vm_read_overwrite(self.task, (C.vm_address_t)(offset), + C.ulong(len(buff)), (*C.char)(unsafe.Pointer(&buff[0])), + &size) + if kr != 0 { + return int(size), io.EOF + } + + return int(size), nil +} + +func (self darwinProcessReader) Close() error { + C.mach_port_deallocate(C.get_task_self(), self.task) + return nil +} + +func (self *ProcessAccessor) OpenWithOSPath( + path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + if len(path.Components) == 0 { + return nil, errors.New("Unable to list all processes, use the pslist() plugin.") + } + + pid, err := strconv.ParseUint(path.Components[0], 0, 64) + if err != nil { + return nil, errors.New("First directory path must be a process.") + } + + reader := &darwinProcessReader{} + + kr := C.task_for_pid(C.get_task_self(), C.int(pid), &reader.task) + if kr != 0 { + return nil, fmt.Errorf("process: Can not open pid %v: %v", pid, kr) + } + + ranges, err := GetVads(pid, reader.task) + if err != nil { + return nil, err + } + + result := &ProcessReader{ + pid: pid, + ranges: ranges, + handle: reader, + } + return result, nil +} + +func GetVads(pid uint64, task C.task_t) (res []*uploads.Range, err error) { + var address C.vm_address_t + var info_count C.mach_msg_type_number_t = VM_REGION_BASIC_INFO_COUNT_64 + var object C.mach_port_t + var info C.vm_region_basic_info_data_t + var size C.vm_size_t + + // Iterate through the address space getting all the regions. + for { + kr := C.vm_region_64(task, &address, + &size, VM_REGION_BASIC_INFO, + (*C.int)(unsafe.Pointer(&info)), &info_count, &object) + if kr != 0 { + break + } + + res = append(res, &uploads.Range{ + Offset: int64(address), + Length: int64(size), + }) + address += size + size = 0 + } + return res, nil +} diff --git a/accessors/process/process_address_space_darwin_nocgo.go b/accessors/process/process_address_space_darwin_nocgo.go new file mode 100644 index 000000000..df8296580 --- /dev/null +++ b/accessors/process/process_address_space_darwin_nocgo.go @@ -0,0 +1,19 @@ +//go:build darwin && !cgo +// +build darwin,!cgo + +package process + +import ( + "errors" + + "www.velocidex.com/golang/velociraptor/accessors" +) + +var ( + notSupportedError = errors.New("ProcessAccessor: This binary is not build with cgo support. Process access not enabled.") +) + +func (self *ProcessAccessor) OpenWithOSPath( + path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + return nil, notSupportedError +} diff --git a/accessors/process/process_address_space_linux.go b/accessors/process/process_address_space_linux.go new file mode 100644 index 000000000..3ecd902c5 --- /dev/null +++ b/accessors/process/process_address_space_linux.go @@ -0,0 +1,102 @@ +//go:build linux +// +build linux + +package process + +import ( + "bufio" + "errors" + "fmt" + "os" + "regexp" + "strconv" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/uploads" +) + +func (self *ProcessAccessor) OpenWithOSPath( + path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + if len(path.Components) == 0 { + return nil, errors.New("Unable to list all processes, use the pslist() plugin.") + } + + pid, err := strconv.ParseUint(path.Components[0], 0, 64) + if err != nil { + return nil, errors.New("First directory path must be a process.") + } + + // Open the device file for the process + fd, err := os.Open(fmt.Sprintf("/proc/%d/mem", pid)) + if err != nil { + return nil, err + } + + // Open the process and enumerate its ranges + ranges, err := GetVads(pid) + if err != nil { + return nil, err + } + result := &ProcessReader{ + pid: pid, + handle: fd, + } + + for _, r := range ranges { + result.ranges = append(result.ranges, r) + } + + return result, nil +} + +var ( + maps_regexp = regexp.MustCompile(`(?P^[^-]+)-(?P[^\s]+)\s+(?P[^\s]+)\s+(?P[^\s]+)\s+[^\s]+\s+(?P[^\s]+)\s+(?P.+?)(?P \(deleted\))?$`) +) + +func GetVads(pid uint64) ([]*uploads.Range, error) { + maps_fd, err := os.Open(fmt.Sprintf("/proc/%d/maps", pid)) + if err != nil { + return nil, err + } + defer maps_fd.Close() + + var result []*uploads.Range + + scanner := bufio.NewScanner(maps_fd) + for scanner.Scan() { + hits := maps_regexp.FindStringSubmatch(scanner.Text()) + if len(hits) > 0 { + protection := hits[3] + // Only include readable ranges. + if len(protection) < 2 || protection[0] != 'r' { + continue + } + + start, err := strconv.ParseInt(hits[1], 16, 64) + if err != nil { + continue + } + + end, err := strconv.ParseInt(hits[2], 16, 64) + if err != nil { + continue + } + + // We can not read kernel memory + if start < 0 || end < 0 { + continue + } + + result = append(result, &uploads.Range{ + Offset: start, Length: end - start, + }) + } + } + + err = scanner.Err() + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/accessors/process/process_address_space_windows.go b/accessors/process/process_address_space_windows.go new file mode 100644 index 000000000..8ab7d90aa --- /dev/null +++ b/accessors/process/process_address_space_windows.go @@ -0,0 +1,410 @@ +//go:build windows && amd64 && cgo +// +build windows,amd64,cgo + +// An accessor for process address space. +// Using this accessor it is possible to read directly from different processes, e.g. +// read_file(filename="/434", accessor="process") + +// Ensure this does not leak handles: +// SELECT * FROM Artifact.Windows.System.VAD( +// SuspiciousContent=''' +// rule Hit { strings: $a = "microsoft" nocase wide ascii condition: any of them }''') +// WHERE FALSE + +// Check the handles we have open in a notebook +// SELECT * FROM handles(pid=getpid()) WHERE Type =~"process" + +package process + +import ( + "context" + "errors" + "os" + "strconv" + "sync" + "syscall" + "time" + + "github.com/Velocidex/ttlcache/v2" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/uploads" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/windows" + "www.velocidex.com/golang/velociraptor/vql/windows/process" + "www.velocidex.com/golang/vfilter" +) + +var ( + processAccessorCurrentOpened = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "accessor_process_current_open", + Help: "Number of currently opened processes", + }) + + processAccessorTotalOpened = promauto.NewCounter(prometheus.CounterOpts{ + Name: "accessor_process_total_open", + Help: "Total Number of opened processes", + }) + + processAccessorTotalReadProcessMemory = promauto.NewCounter(prometheus.CounterOpts{ + Name: "accessor_process_total_read_process_memory", + Help: "Total Number of opened buffers read from process memory", + }) +) + +const PAGE_SIZE = 0x1000 + +type ProcessReader struct { + mu sync.Mutex + pid uint64 + offset uint64 + size uint64 + handle syscall.Handle + ranges []*process.VMemInfo + last_range *process.VMemInfo + + in_use int +} + +func (self *ProcessReader) getRange(offset uint64) *process.VMemInfo { + if self.last_range != nil && + self.last_range.Address <= offset && + offset < self.last_range.Address+self.last_range.Size { + return self.last_range + } + + // TODO: Is it worth to implement a binary search here? + for i := 0; i < len(self.ranges); i++ { + self.last_range = self.ranges[i] + + // Does the range cover the require offset? + if self.last_range.Address <= offset && + offset < self.last_range.Address+self.last_range.Size { + return self.last_range + } + + // Use the fact that ranges are sorted to break early. + if offset < self.last_range.Address { + break + } + } + return nil +} + +// Repeat the read operation one page at the time in order to retrieve +// as much data as possible. +func (self *ProcessReader) readDistinctPages(buf []byte) (int, error) { + page_count := len(buf) / PAGE_SIZE + if page_count <= 1 { + // Buffer is smaller than pagesize, just return a null buffer + return len(buf), nil + } + + // Read as many pages as possible into the buffer ignoring errors. + for i := 0; i < page_count; i += 1 { + buf_start := i * PAGE_SIZE + buf_end := buf_start + PAGE_SIZE + + // Repeat the read with a single page at the time. + _, err := windows.ReadProcessMemory( + self.handle, self.offset, buf[buf_start:buf_end]) + if err != nil { + // Error occured reading a single page, zero + // it out and skip the page. + for i := buf_start; i < buf_end; i++ { + buf[i] = 0 + } + self.offset += PAGE_SIZE + } + } + + return page_count * PAGE_SIZE, nil +} + +func (self *ProcessReader) Read(buf []byte) (int, error) { + self.mu.Lock() + defer self.mu.Unlock() + + current_range := self.getRange(self.offset) + if current_range == nil { + return 0, errors.New("Invalid offset") + } + + to_read := current_range.Address + current_range.Size - self.offset + if to_read > uint64(len(buf)) { + to_read = uint64(len(buf)) + } + + processAccessorTotalReadProcessMemory.Inc() + + // Read memory from process at specified offset. + _, err := windows.ReadProcessMemory( + self.handle, self.offset, buf[:to_read]) + + // A read error occured - split the read into multiple page + // size reads to get as much data as we can out of the + // region. Note: We always return as much data as was + // required, we simply null pad the missing data. Therefore if + // a reader askes to read from a memory region that contains + // no data, we never return an error - just zero pad those + // regions. + if err != nil { + res, err := self.readDistinctPages(buf) + return res, err + } + + // Advance the read pointer. + self.offset += to_read + + return int(to_read), nil +} + +func (self *ProcessReader) Ranges() []uploads.Range { + self.mu.Lock() + defer self.mu.Unlock() + + result := []uploads.Range{} + size := uint64(0) + for _, rng := range self.ranges { + // Only include readable ranges. + if len(rng.Protection) < 2 || rng.Protection[1] != 'r' { + continue + } + + // Fill in a sparse range if needed + if rng.Address > size { + result = append(result, uploads.Range{ + Offset: int64(size), + Length: int64(rng.Address - size), + IsSparse: true, + }) + } + + // Move the pointer past the end of this range. + size = rng.Address + rng.Size + + // Add a real data run + result = append(result, uploads.Range{ + Offset: int64(rng.Address), + Length: int64(rng.Size), + IsSparse: false, + }) + } + return result +} + +func (self *ProcessReader) Seek(offset int64, whence int) (int64, error) { + self.mu.Lock() + defer self.mu.Unlock() + + switch whence { + case 0: + self.offset = uint64(offset) + case 1: + self.offset += uint64(offset) + case 2: + self.offset = self.size + } + + return int64(self.offset), nil +} + +// Keep the process alive in cache for a bit +func (self *ProcessReader) Close() error { + self.mu.Lock() + defer self.mu.Unlock() + + self.in_use-- + return nil +} + +// The cache will close this process properly. +func (self *ProcessReader) closeCache() error { + // Mark it as closed + self.in_use = -100 + + processAccessorCurrentOpened.Dec() + return windows.CloseHandle(self.handle) +} + +func (self ProcessReader) Stat() (os.FileInfo, error) { + return &accessors.VirtualFileInfo{Size_: int64(self.size)}, nil +} + +type ProcessAccessor struct { + mu sync.Mutex + lru *ttlcache.Cache + scope vfilter.Scope +} + +const _ProcessAccessorTag = "_ProcessAccessor" + +func (self ProcessAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + self.mu.Lock() + defer self.mu.Unlock() + + result_any := vql_subsystem.CacheGet(scope, _ProcessAccessorTag) + if result_any == nil { + // Create a new cache in the scope. + result := &ProcessAccessor{ + lru: ttlcache.NewCache(), + scope: scope, + } + result.lru.SetTTL(time.Second) + result.lru.SkipTTLExtensionOnHit(true) + result.lru.SetCheckExpirationCallback(func(key string, value interface{}) bool { + info, ok := value.(*ProcessReader) + if ok { + info.mu.Lock() + defer info.mu.Unlock() + + // Reader is in use do not allow it to expire. + if info.in_use > 0 { + return false + } + + // No one is using it, close the handle + if info.in_use == 0 { + info.closeCache() + } + } + return true + }) + + vql_subsystem.CacheSet(scope, _ProcessAccessorTag, result) + + vql_subsystem.GetRootScope(scope).AddDestructor(func() { + // Force the lru to expire even if readers are still in + // use! This ensures we do not leak handles. + for _, k := range result.lru.GetKeys() { + v, err := result.lru.Get(k) + if err == nil { + reader := v.(*ProcessReader) + + reader.mu.Lock() + reader.closeCache() + reader.mu.Unlock() + } + } + result.lru.Close() + }) + return result, nil + } + + return result_any.(*ProcessAccessor), nil +} + +func (self ProcessAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "process", + Description: `Access process memory like a file. The Path is taken in the form "/", i.e. the pid appears as the top level file.`, + Permissions: []acls.ACL_PERMISSION{acls.MACHINE_STATE}, + } +} + +func (self ProcessAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewLinuxOSPath(path) +} + +func (self ProcessAccessor) ReadDir(path string) ([]accessors.FileInfo, error) { + return nil, errors.New("Unable to list all processes, use the pslist() plugin.") +} + +func (self ProcessAccessor) ReadDirWithOSPath( + path *accessors.OSPath) ([]accessors.FileInfo, error) { + return nil, errors.New("Unable to list all processes, use the pslist() plugin.") +} + +func (self ProcessAccessor) Lstat(filename string) (accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return &accessors.VirtualFileInfo{ + Path: full_path, + }, nil +} + +func (self ProcessAccessor) LstatWithOSPath(full_path *accessors.OSPath) ( + accessors.FileInfo, error) { + + return &accessors.VirtualFileInfo{ + Path: full_path, + }, nil +} + +func (self *ProcessAccessor) Open(filename string) ( + accessors.ReadSeekCloser, error) { + + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self *ProcessAccessor) OpenWithOSPath( + full_path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + + if len(full_path.Components) == 0 { + return nil, errors.New("Unable to list all processes, use the pslist() plugin.") + } + + pid_str := full_path.Components[0] + pid, err := strconv.ParseUint(pid_str, 0, 64) + if err != nil { + return nil, errors.New("First directory path must be a process.") + } + + self.mu.Lock() + defer self.mu.Unlock() + + cached_any, err := self.lru.Get(pid_str) + if err == nil { + info := cached_any.(*ProcessReader) + info.mu.Lock() + + // If the handle is already closed make a new one. + if info.in_use >= 0 { + info.in_use++ + info.mu.Unlock() + return info, nil + } + info.mu.Unlock() + } + + // Open the process and enumerate its ranges + ranges, proc_handle, err := process.GetVads( + context.Background(), self.scope, uint32(pid)) + if err != nil { + return nil, err + } + + processAccessorCurrentOpened.Inc() + processAccessorTotalOpened.Inc() + + result := &ProcessReader{ + pid: pid, + handle: proc_handle, + in_use: 1, // One user as we just return it to our caller. + } + + for _, r := range ranges { + result.ranges = append(result.ranges, r) + } + + // Cache for next time. + self.lru.Set(pid_str, result) + + return result, nil +} + +func init() { + accessors.Register(&ProcessAccessor{}) +} diff --git a/accessors/protocols.go b/accessors/protocols.go new file mode 100644 index 000000000..a101d5bf9 --- /dev/null +++ b/accessors/protocols.go @@ -0,0 +1,263 @@ +package accessors + +import ( + "context" + "reflect" + + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/protocols" +) + +type _BoolOSPath struct{} + +func (self _BoolOSPath) Applicable(a vfilter.Any) bool { + _, ok := a.(*OSPath) + return ok +} + +func (self _BoolOSPath) Bool(ctx context.Context, scope vfilter.Scope, a vfilter.Any) bool { + os_path, ok := a.(*OSPath) + return ok && (len(os_path.Components) > 0 || + os_path.DelegatePath() != "") +} + +type _BoolFileInfo struct{} + +func (self _BoolFileInfo) Applicable(a vfilter.Any) bool { + _, ok := a.(FileInfo) + return ok +} + +func (self _BoolFileInfo) Bool( + ctx context.Context, scope vfilter.Scope, a vfilter.Any) bool { + return true +} + +type _EqualOSPath struct{} + +func (self _EqualOSPath) Applicable(a vfilter.Any, b vfilter.Any) bool { + _, ok := a.(*OSPath) + if !ok { + return false + } + _, ok = b.(*OSPath) + return ok +} + +func (self _EqualOSPath) Eq(scope vfilter.Scope, a vfilter.Any, b vfilter.Any) bool { + a_os_path, ok := a.(*OSPath) + if !ok { + return false + } + + b_os_path, ok := b.(*OSPath) + if !ok { + return false + } + + if !utils.StringSliceEq(a_os_path.Components, b_os_path.Components) { + return false + } + + return a_os_path.String() == b_os_path.String() +} + +type _LtOSPath struct{} + +func (self _LtOSPath) Applicable(a vfilter.Any, b vfilter.Any) bool { + _, ok := a.(*OSPath) + if !ok { + return false + } + _, ok = b.(*OSPath) + return ok +} + +func (self _LtOSPath) Lt(scope vfilter.Scope, a vfilter.Any, b vfilter.Any) bool { + a_os_path, ok := a.(*OSPath) + if !ok { + return false + } + + b_os_path, ok := b.(*OSPath) + if !ok { + return false + } + return a_os_path.String() < b_os_path.String() +} + +type _RegexOSPath struct{} + +func (self _RegexOSPath) Applicable(a vfilter.Any, b vfilter.Any) bool { + _, ok := b.(*OSPath) + if !ok { + return false + } + _, ok = a.(string) + return ok +} + +func (self _RegexOSPath) Match(scope vfilter.Scope, a vfilter.Any, b vfilter.Any) bool { + b_os_path, ok := b.(*OSPath) + if !ok { + return false + } + + // Shortcut for matches against "." or an empty string will match + // anything - we do not need to expand the OSPath + a_str, ok := a.(string) + if !ok || a_str == "" || a_str == "." { + return true + } + + return scope.Match(a, b_os_path.String()) +} + +type _AddOSPath struct{} + +func (self _AddOSPath) Applicable(a vfilter.Any, b vfilter.Any) bool { + _, ok := a.(*OSPath) + if !ok { + return false + } + + switch b.(type) { + case *OSPath, string: + return true + } + + a_value := reflect.Indirect(reflect.ValueOf(b)) + + return a_value.Type().Kind() == reflect.Slice +} + +func (self _AddOSPath) Add(scope vfilter.Scope, a vfilter.Any, b vfilter.Any) vfilter.Any { + a_os_path, ok := a.(*OSPath) + if !ok { + return false + } + + switch t := b.(type) { + case *OSPath: + return a_os_path.Append(t.Components...) + + case string: + parsed, err := ParsePath(t, "") + if err != nil { + return vfilter.Null{} + } + return a_os_path.Append(parsed.Components...) + } + + a_value := reflect.Indirect(reflect.ValueOf(b)) + if a_value.Type().Kind() == reflect.Slice { + components := []string{} + for idx := 0; idx < a_value.Len(); idx++ { + item := a_value.Index(int(idx)).Interface() + str_item, ok := item.(string) + if ok { + components = append(components, str_item) + } + } + + return a_os_path.Append(components...) + } + + return a_os_path +} + +type _AssociativeOSPath struct{} + +// Filter some method calls to be more useful. +func (self _AssociativeOSPath) Applicable(a vfilter.Any, b vfilter.Any) bool { + _, ok := a.(*OSPath) + if !ok { + return false + } + switch b.(type) { + case []*int64, string, int64: + return true + } + return false +} + +func (self _AssociativeOSPath) Associative( + scope vfilter.Scope, a vfilter.Any, b vfilter.Any) (vfilter.Any, bool) { + a_os_path, ok := a.(*OSPath) + if !ok { + return &vfilter.Null{}, false + } + + length := int64(len(a_os_path.Components)) + + switch t := b.(type) { + case []*int64: + first_item := int64(0) + if t[0] != nil { + first_item = *t[0] + } + + second_item := length + if t[1] != nil { + second_item = *t[1] + if second_item > length { + second_item = length + } + } + + // Wrap around behavior for negative index. + if first_item < 0 { + first_item += length + } + + if second_item < 0 { + second_item += length + } + + if second_item <= first_item { + return a_os_path.Clear(), true + } + + return a_os_path.Clear().Append( + a_os_path.Components[first_item:second_item]...), true + + case int64: + if t < 0 { + t += length + } + if t < 0 || t >= length { + return &vfilter.Null{}, true + } + return a_os_path.Components[t], true + + case string: + switch t { + case "HumanString": + return a_os_path.HumanString(scope), true + + default: + return protocols.DefaultAssociative{}.Associative(scope, a, b) + } + + default: + return protocols.DefaultAssociative{}.Associative(scope, a, b) + } +} + +func (self _AssociativeOSPath) GetMembers(scope vfilter.Scope, a vfilter.Any) []string { + result := protocols.DefaultAssociative{}.GetMembers(scope, a) + result = append(result, "HumanString") + return result +} + +func init() { + vql_subsystem.RegisterProtocol(&_BoolOSPath{}) + vql_subsystem.RegisterProtocol(&_BoolFileInfo{}) + vql_subsystem.RegisterProtocol(&_EqualOSPath{}) + vql_subsystem.RegisterProtocol(&_LtOSPath{}) + vql_subsystem.RegisterProtocol(&_AddOSPath{}) + vql_subsystem.RegisterProtocol(&_RegexOSPath{}) + vql_subsystem.RegisterProtocol(&_AssociativeOSPath{}) +} diff --git a/accessors/pst/cache.go b/accessors/pst/cache.go new file mode 100644 index 000000000..f84127768 --- /dev/null +++ b/accessors/pst/cache.go @@ -0,0 +1,291 @@ +//go:build !arm && !mips && !(linux && 386) +// +build !arm +// +build !mips +// +build !linux !386 + +package pst + +import ( + "errors" + "fmt" + "path" + "sync" + "time" + + "github.com/Velocidex/ttlcache/v2" + pst "github.com/mooijtech/go-pst/v6/pkg" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/utils/files" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +const ( + PSTCacheTag = "_PST_CACHE" +) + +var ( + InUseError = errors.New("In Use") +) + +type PSTFile struct { + *pst.File + + // Reference count reader + mu sync.Mutex + refs int + reader accessors.ReadSeekCloser + + // Maintain an index of everything for fast access. + paths map[pst.Identifier]string + attachments map[pst.Identifier]*pst.Attachment + + // For tracking file opens + key string +} + +func (self *PSTFile) GetPath(id pst.Identifier) string { + self.mu.Lock() + defer self.mu.Unlock() + + res, _ := self.paths[id] + return res +} + +func (self *PSTFile) setPath(parent_id, id pst.Identifier, name string) { + root, _ := self.paths[parent_id] + new_path := path.Join(root, name) + self.paths[id] = new_path +} + +func (self *PSTFile) walkFolders(folder *pst.Folder) error { + subFolders, err := folder.GetSubFolders() + if err != nil { + return err + } + + for _, subFolder := range subFolders { + self.setPath(folder.Identifier, subFolder.Identifier, subFolder.Name) + + // Walk the children + err := self.walkFolders(&subFolder) + if err != nil { + return err + } + + messageIterator, err := subFolder.GetMessageIterator() + if err != nil { + continue + } + + for messageIterator.Next() { + message := messageIterator.Value() + self.setPath(subFolder.Identifier, message.Identifier, + fmt.Sprintf("Msg-%d", message.Identifier)) + + attachmentIterator, err := message.GetAttachmentIterator() + if err != nil { + continue + } + + for attachmentIterator.Next() { + attachment := attachmentIterator.Value() + self.setPath(message.Identifier, attachment.Identifier, + fmt.Sprintf("Att-%d", attachment.Identifier)) + + self.attachments[attachment.Identifier] = attachment + } + } + + } + return nil +} + +func (self *PSTFile) Initialize() error { + self.mu.Lock() + defer self.mu.Unlock() + + rootFolder, err := self.GetRootFolder() + if err != nil { + return err + } + + self.setPath(rootFolder.Identifier, rootFolder.Identifier, rootFolder.Name) + return self.walkFolders(&rootFolder) +} + +func (self *PSTFile) IncRef() { + self.mu.Lock() + defer self.mu.Unlock() + + self.refs++ +} + +func (self *PSTFile) ForceClose() { + self.reader.Close() +} + +func (self *PSTFile) TryToClose() error { + self.mu.Lock() + defer self.mu.Unlock() + + if self.refs != 0 { + return InUseError + } + + self.File.Cleanup() + self.reader.Close() + files.Remove(self.key) + + return nil +} + +func (self *PSTFile) Close() { + self.mu.Lock() + defer self.mu.Unlock() + + self.refs-- +} + +// Used to open a fixed attachement for reading. +func (self *PSTFile) GetAttachment(att_id pst.Identifier) ( + res *pst.Attachment, closer func(), err error) { + + self.mu.Lock() + defer self.mu.Unlock() + + res, pres := self.attachments[att_id] + if !pres { + return nil, nil, utils.NotFoundError + } + self.refs++ + + // Keep track of open files. + key := fmt.Sprintf("%v-%d", self.key, att_id) + files.Add(key) + + return res, func() { + self.Close() + files.Remove(key) + }, nil +} + +type PSTCache struct { + mu sync.Mutex + + // key: Accessor/Pathspec Value: PSTFile + lru *ttlcache.Cache +} + +func (self *PSTCache) Close() { + self.mu.Lock() + defer self.mu.Unlock() + + self.lru.Close() +} + +// Opens the PST file or get it from cache. +func (self *PSTCache) Open( + scope vfilter.Scope, + accessor_name string, path *accessors.OSPath) (*PSTFile, error) { + + self.mu.Lock() + defer self.mu.Unlock() + + key := accessor_name + path.String() + pst_file_any, err := self.lru.Get(key) + // Cache hit + if err == nil { + res := pst_file_any.(*PSTFile) + res.IncRef() + return res, nil + } + + // Open it the old fasioned way. + accessor, err := accessors.GetAccessor(accessor_name, scope) + if err != nil { + return nil, err + } + + reader, err := accessor.OpenWithOSPath(path) + if err != nil { + return nil, err + } + files.Add(key) + + // Closed by the PSTFile when refs are zero and cache timeout is + // reached. + + pstFile, err := pst.New(utils.MakeReaderAtter(reader)) + if err != nil { + return nil, err + } + + // Cache the file + res := &PSTFile{ + File: pstFile, + reader: reader, + attachments: make(map[pst.Identifier]*pst.Attachment), + paths: make(map[pst.Identifier]string), + key: key, + } + err = res.Initialize() + if err != nil { + return nil, err + } + + res.IncRef() + + return res, self.lru.Set(key, res) +} + +func GetPSTCache(scope vfilter.Scope) *PSTCache { + cache, ok := vql_subsystem.CacheGet(scope, PSTCacheTag).(*PSTCache) + if ok { + return cache + } + + cache_size := int(vql_subsystem.GetIntFromRow( + scope, scope, constants.PST_CACHE_SIZE)) + if cache_size == 0 { + cache_size = 20 + } + + // Cache is disabled. + if cache_size < 0 { + return &PSTCache{} + } + + cache_time := vql_subsystem.GetIntFromRow( + scope, scope, constants.PST_CACHE_TIME) + if cache_time == 0 { + cache_time = 60 + } + + cache = &PSTCache{ + lru: ttlcache.NewCache(), + } + + cache.lru.SetCacheSizeLimit(cache_size) + _ = cache.lru.SetTTL(time.Second * time.Duration(cache_time)) + + cache.lru.SetExpirationCallback( + func(key string, value interface{}) error { + ctx, ok := value.(*PSTFile) + if ok { + // Do not block the lru while closing. + go ctx.Close() + } + return nil + }) + + root_scope := vql_subsystem.GetRootScope(scope) + _ = root_scope.AddDestructor(func() { + cache.Close() + }) + vql_subsystem.CacheSet(root_scope, PSTCacheTag, cache) + + return cache +} diff --git a/accessors/pst/doc.go b/accessors/pst/doc.go new file mode 100644 index 000000000..1a5645199 --- /dev/null +++ b/accessors/pst/doc.go @@ -0,0 +1,3 @@ +package pst + +// Parser for PST files diff --git a/accessors/pst/pst_accessor.go b/accessors/pst/pst_accessor.go new file mode 100644 index 000000000..861991cc3 --- /dev/null +++ b/accessors/pst/pst_accessor.go @@ -0,0 +1,172 @@ +//go:build !arm && !mips && !(linux && 386) +// +build !arm +// +build !mips +// +build !linux !386 + +package pst + +import ( + "errors" + "io" + "strconv" + "strings" + + pst "github.com/mooijtech/go-pst/v6/pkg" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +type ReaderWrapper struct { + io.ReadSeeker + closer func() +} + +func (self *ReaderWrapper) Close() error { + self.closer() + return nil +} + +type PSTFileSystemAccessor struct { + scope vfilter.Scope +} + +func (self PSTFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewGenericOSPath(path) +} + +func (self PSTFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "pst", + Description: `An accessor to open attachments in PST files. + +This accessor allows opening of attachments for scanning or reading. + +The OSPath used is structured in the form: + +{ + Path: "Msg//Att//filename", + DelegatePath: , + DelegateAccessor: +} +`, + Permissions: []acls.ACL_PERMISSION{acls.FILESYSTEM_READ}, + } +} + +func (self PSTFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + return &PSTFileSystemAccessor{scope: scope}, nil +} + +func (self PSTFileSystemAccessor) ReadDir(path string) ( + []accessors.FileInfo, error) { + return nil, errors.New("Unable to list all MFT entries.") +} + +func (self PSTFileSystemAccessor) ReadDirWithOSPath(path *accessors.OSPath) ( + []accessors.FileInfo, error) { + return nil, errors.New("Unable to list all MFT entries.") +} + +func (self *PSTFileSystemAccessor) Open(path string) ( + accessors.ReadSeekCloser, error) { + + full_path, err := self.ParsePath(path) + if err != nil || len(full_path.Components) == 0 { + return nil, utils.NotFoundError + } + + return self.OpenWithOSPath(full_path) +} + +func (self *PSTFileSystemAccessor) OpenWithOSPath( + full_path *accessors.OSPath) ( + accessors.ReadSeekCloser, error) { + + attachment, closer, err := self.getAttachment(full_path) + if err != nil { + return nil, err + } + + attachmentReader, err := attachment.PropertyContext.GetPropertyReader( + 14081, attachment.LocalDescriptors) + if err != nil { + return nil, err + } + + return &ReaderWrapper{ + ReadSeeker: io.NewSectionReader(&attachmentReader, 0, attachmentReader.Size()), + closer: closer, + }, nil +} + +func (self *PSTFileSystemAccessor) Lstat(path string) ( + accessors.FileInfo, error) { + full_path, err := self.ParsePath(path) + if err != nil || len(full_path.Components) == 0 { + return nil, utils.NotFoundError + } + + return self.LstatWithOSPath(full_path) +} + +func (self *PSTFileSystemAccessor) getAttachment( + path *accessors.OSPath) (res *pst.Attachment, closer func(), err error) { + + if len(path.Components) == 0 { + return nil, nil, utils.NotFoundError + } + + filename := path.Components[len(path.Components)-1] + if !strings.HasPrefix(filename, "Att-") { + return nil, nil, utils.Wrap( + utils.NotFoundError, "Invalid Path format for PST accessor") + } + + att_id, err := strconv.ParseInt(filename[4:], 0, 64) + if err != nil { + return nil, nil, utils.Wrap( + utils.NotFoundError, "Invalid Path format for PST accessor") + } + + pst_cache := GetPSTCache(self.scope) + delegate, err := path.Delegate(self.scope) + if err != nil { + return nil, nil, err + } + + pstFile, err := pst_cache.Open(self.scope, path.DelegateAccessor(), delegate) + if err != nil { + return nil, nil, err + } + + return pstFile.GetAttachment(pst.Identifier(att_id)) +} + +func (self *PSTFileSystemAccessor) LstatWithOSPath(full_path *accessors.OSPath) ( + accessors.FileInfo, error) { + + attachment, closer, err := self.getAttachment(full_path) + if err != nil { + return nil, err + } + defer closer() + + attachmentReader, err := attachment.PropertyContext.GetPropertyReader( + 14081, attachment.LocalDescriptors) + if err != nil { + return nil, err + } + + return &accessors.VirtualFileInfo{ + Size_: attachmentReader.Size(), + Path: full_path, + }, nil +} + +func init() { + accessors.Register(&PSTFileSystemAccessor{}) +} diff --git a/accessors/raw_file/raw_file.go b/accessors/raw_file/raw_file.go new file mode 100644 index 000000000..05edd9cc1 --- /dev/null +++ b/accessors/raw_file/raw_file.go @@ -0,0 +1,145 @@ +/* This accessor is used for reading raw devices. + +On Windows, raw files need to be read in aligned page size. This +accessor ensures reads are buffered into page size buffers to make it +safe for VQL to read the device in arbitrary alignment. + +We do not support directory operations on raw devices. + +*/ + +package raw_file + +import ( + "errors" + "fmt" + "os" + + ntfs "www.velocidex.com/golang/go-ntfs/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/file" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/utils/files" + "www.velocidex.com/golang/vfilter" +) + +type RawFileSystemAccessor struct { + scope vfilter.Scope +} + +func (self RawFileSystemAccessor) ParsePath(path string) (*accessors.OSPath, error) { + return accessors.NewRawFilePath(path) +} + +func (self RawFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + result := &RawFileSystemAccessor{ + scope: scope, + } + return result, nil +} + +func (self RawFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "raw_file", + Description: `Access a device using aligned reads. + +On Windows device reads must be aligned to page size. + +This accessor ensures all reads are aligned. + +The accessor may be used on other files but: + +1. Reads will be aligned to page size (4096 bytes) +2. The last page will be zero padded past the end of file. +`, + Permissions: []acls.ACL_PERMISSION{acls.FILESYSTEM_READ}, + } +} + +func (self RawFileSystemAccessor) ReadDir( + path string) ([]accessors.FileInfo, error) { + return nil, errors.New("Not Implemented") +} + +func (self RawFileSystemAccessor) ReadDirWithOSPath( + path *accessors.OSPath) ([]accessors.FileInfo, error) { + return nil, errors.New("Not Implemented") +} + +func (self RawFileSystemAccessor) OpenWithOSPath( + filename *accessors.OSPath) (accessors.ReadSeekCloser, error) { + return self.Open(filename.Path()) +} + +func (self RawFileSystemAccessor) Open(filename string) (accessors.ReadSeekCloser, error) { + // Treat the path as a raw OS path. + file, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("While opening %v: %v", filename, err) + } + + files.Add(filename) + + reader, err := ntfs.NewPagedReader(file, 0x1000, 10000) + if err != nil { + return nil, err + } + + res := utils.NewReadSeekReaderAdapter(reader, func() { + files.Remove(filename) + }) + + // Try to figure out the size - not necessary but in case we can + // we can limit readers to this size. + stat, err := os.Lstat(filename) + if err == nil { + res.SetSize(stat.Size()) + } + + return res, nil +} + +func (self RawFileSystemAccessor) Lstat(path string) (accessors.FileInfo, error) { + full_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + stat, err := os.Lstat(path) + if err != nil { + // On Windows it is not always possible to stat a device. In + // that case we need to return a fake object so it is not an + // error. + stat = &accessors.VirtualFileInfo{ + Path: full_path, + Size_: 1<<63 - 1, + } + } + + return file.NewOSFileInfo(stat, full_path), err +} + +func (self RawFileSystemAccessor) LstatWithOSPath( + full_path *accessors.OSPath) (accessors.FileInfo, error) { + + path := full_path.String() + stat, err := os.Lstat(path) + if err != nil { + // On Windows it is not always possible to stat a device. In + // that case we need to return a fake object so it is not an + // error. + stat = &accessors.VirtualFileInfo{ + Path: full_path, + Size_: 1<<63 - 1, + } + } + + return file.NewOSFileInfo(stat, full_path), err +} + +func init() { + accessors.Register(&RawFileSystemAccessor{}) +} diff --git a/accessors/raw_registry/cache.go b/accessors/raw_registry/cache.go new file mode 100644 index 000000000..f30e09138 --- /dev/null +++ b/accessors/raw_registry/cache.go @@ -0,0 +1,78 @@ +package raw_registry + +import ( + "time" + + "github.com/Velocidex/ttlcache/v2" + "www.velocidex.com/golang/velociraptor/constants" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +const ( + RAW_CACHE_TAG = "_RAW_REG_CACHE" +) + +type RawRegFileSystemAccessorCache struct { + lru *ttlcache.Cache + readdir_lru *ttlcache.Cache +} + +func (self *RawRegFileSystemAccessorCache) Close() { + self.lru.Close() + self.readdir_lru.Close() +} + +func getRegFileSystemAccessorCache(scope vfilter.Scope) *RawRegFileSystemAccessorCache { + cache, ok := vql_subsystem.CacheGet( + scope, RAW_CACHE_TAG).(*RawRegFileSystemAccessorCache) + if ok { + return cache + } + + cache = &RawRegFileSystemAccessorCache{ + lru: ttlcache.NewCache(), + readdir_lru: ttlcache.NewCache(), + } + + cache_size := int(vql_subsystem.GetIntFromRow( + scope, scope, constants.RAW_REG_CACHE_SIZE)) + if cache_size == 0 { + cache_size = 1000 + } + + cache_time := vql_subsystem.GetIntFromRow( + scope, scope, constants.RAW_REG_CACHE_TIME) + if cache_time == 0 { + cache_time = 10 + } + + cache.lru.SetCacheSizeLimit(cache_size) + _ = cache.lru.SetTTL(time.Second * time.Duration(cache_time)) + cache.lru.SkipTTLExtensionOnHit(true) + + cache.readdir_lru.SetCacheSizeLimit(cache_size) + _ = cache.readdir_lru.SetTTL(time.Second * time.Duration(cache_time)) + cache.readdir_lru.SkipTTLExtensionOnHit(true) + + // Add the cache to the root scope so it can be visible outside + // our scope. This should maximize cache hits + root_scope := vql_subsystem.GetRootScope(scope) + + err := root_scope.AddDestructor(func() { + cache.Close() + cache.lru.Close() + cache.readdir_lru.Close() + }) + if err != nil { + cache.Close() + cache.lru.Close() + cache.readdir_lru.Close() + + return cache + } + + vql_subsystem.CacheSet(root_scope, RAW_CACHE_TAG, cache) + + return cache +} diff --git a/accessors/raw_registry/fixtures/TestAccessorRawReg.golden b/accessors/raw_registry/fixtures/TestAccessorRawReg.golden new file mode 100644 index 000000000..fbea9f2b3 --- /dev/null +++ b/accessors/raw_registry/fixtures/TestAccessorRawReg.golden @@ -0,0 +1,13 @@ +[ + "SAM\\Domains\\Account\\Aliases", + "SAM\\Domains\\Account\\F", + "SAM\\Domains\\Account\\Groups", + "SAM\\Domains\\Account\\Users", + "SAM\\Domains\\Account\\V", + "SAM\\Domains\\Builtin\\Aliases", + "SAM\\Domains\\Builtin\\F", + "SAM\\Domains\\Builtin\\Groups", + "SAM\\Domains\\Builtin\\PerComponentWellKnownAccountAppliedUpdates", + "SAM\\Domains\\Builtin\\Users", + "SAM\\Domains\\Builtin\\V" +] \ No newline at end of file diff --git a/accessors/raw_registry/lru.go b/accessors/raw_registry/lru.go new file mode 100644 index 000000000..55b24f700 --- /dev/null +++ b/accessors/raw_registry/lru.go @@ -0,0 +1,13 @@ +package raw_registry + +import ( + "www.velocidex.com/golang/regparser" + "www.velocidex.com/golang/velociraptor/accessors" +) + +type readDirLRUItem struct { + children []accessors.FileInfo + err error + + key *regparser.CM_KEY_NODE +} diff --git a/accessors/raw_registry/raw_registry.go b/accessors/raw_registry/raw_registry.go new file mode 100644 index 000000000..7a506be22 --- /dev/null +++ b/accessors/raw_registry/raw_registry.go @@ -0,0 +1,618 @@ +/* + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +// A filesystem accessor for accessing registry hives through raw +// file parsing. + +// We make the registry look like a filesystem: +// 1. Keys are mapped as directories, and values are files. +// 2. The file is interpreted as a URL with the following format: +// accessor:/path#key_path +// 3. We use the accessor and path to open the underlying file, then +// extract the key or value named by the key_path from it. +// 4. Normalized paths contain / for directory separators. +// 5. Normalized paths have rawreg: prefix. +package raw_registry + +import ( + "bytes" + "errors" + "os" + "strings" + "sync" + "time" + + "github.com/Velocidex/ordereddict" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "www.velocidex.com/golang/regparser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/readers" + "www.velocidex.com/golang/vfilter" +) + +const ( + MAX_EMBEDDED_REG_VALUE = 4 * 1024 +) + +var ( + metricsReadValue = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "rawreg_getvalue", + Help: "Number of time we Queried Value from the registry", + }) + + metricsReadDirLruHit = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "rawreg_readdir_lru_hit", + Help: "Performance of the Read Dir Cache", + }) + + metricsReadDirLruMiss = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "rawreg_readdir_lru_miss", + Help: "Performance of the Read Dir Cache", + }) +) + +type RawRegKeyInfo struct { + mu sync.Mutex + + _full_path *accessors.OSPath + _data *ordereddict.Dict + _modtime time.Time + + _key *regparser.CM_KEY_NODE +} + +func (self *RawRegKeyInfo) IsDir() bool { + return true +} + +func (self *RawRegKeyInfo) Data() *ordereddict.Dict { + self.mu.Lock() + defer self.mu.Unlock() + + if self._data == nil { + self._data = ordereddict.NewDict().Set("type", "Key") + } + + return self._data +} + +func (self *RawRegKeyInfo) Size() int64 { + return 0 +} + +func (self *RawRegKeyInfo) UniqueName() string { + // Key names can not have \ in them so it is safe to add this + // without risk of collisions. + return self._full_path.String() + "\\" +} + +func (self *RawRegKeyInfo) FullPath() string { + return self._full_path.String() +} + +func (self *RawRegKeyInfo) OSPath() *accessors.OSPath { + return self._full_path +} + +func (self *RawRegKeyInfo) Mode() os.FileMode { + return 0755 | os.ModeDir +} + +func (self *RawRegKeyInfo) Name() string { + return self._full_path.Basename() +} + +func (self *RawRegKeyInfo) ModTime() time.Time { + return self._modtime +} + +func (self *RawRegKeyInfo) Mtime() time.Time { + return self.ModTime() +} + +func (self *RawRegKeyInfo) Ctime() time.Time { + return self.Mtime() +} + +func (self *RawRegKeyInfo) Btime() time.Time { + return self.Mtime() +} + +func (self *RawRegKeyInfo) Atime() time.Time { + return self.Mtime() +} + +// Not supported +func (self *RawRegKeyInfo) IsLink() bool { + return false +} + +func (self *RawRegKeyInfo) GetLink() (*accessors.OSPath, error) { + return nil, errors.New("Not implemented") +} + +func (self *RawRegKeyInfo) UnmarshalJSON(data []byte) error { + return nil +} + +type RawRegValueInfo struct { + // Containing key + *RawRegKeyInfo + + // Hold a reference so value can be decoded lazily. + _value *regparser.CM_KEY_VALUE + + // Once value is decoded once it will be cached here. + _data *ordereddict.Dict + _size int64 +} + +func (self *RawRegValueInfo) Copy() *RawRegValueInfo { + return &RawRegValueInfo{ + RawRegKeyInfo: &RawRegKeyInfo{ + _full_path: self._full_path, + _modtime: self._modtime, + _key: self._key, + }, + _value: self._value, + _data: self._data, + _size: self._size, + } +} + +func (self *RawRegValueInfo) IsDir() bool { + return false +} + +func (self *RawRegValueInfo) UniqueName() string { + return self._full_path.String() +} + +func (self *RawRegValueInfo) Mode() os.FileMode { + return 0644 +} + +func (self *RawRegValueInfo) Size() int64 { + if self._size > 0 { + return self._size + } + self._size = int64(self._value.DataSize()) + return self._size +} + +func (self *RawRegValueInfo) Data() *ordereddict.Dict { + self.mu.Lock() + defer self.mu.Unlock() + + if self._data != nil { + return self._data + } + + metricsReadValue.Inc() + value_data := self._value.ValueData() + value_type := self._value.TypeString() + result := ordereddict.NewDict(). + Set("type", value_type). + Set("data_len", len(value_data.Data)) + + switch value_data.Type { + case regparser.REG_SZ, regparser.REG_EXPAND_SZ: + result.Set("value", strings.TrimRight(value_data.String, "\x00")) + + case regparser.REG_MULTI_SZ: + result.Set("value", value_data.MultiSz) + + case regparser.REG_DWORD, regparser.REG_QWORD, regparser.REG_DWORD_BIG_ENDIAN: + result.Set("value", value_data.Uint64) + default: + if len(value_data.Data) < MAX_EMBEDDED_REG_VALUE { + result.Set("value", value_data.Data) + } + } + + self._data = result + return result +} + +type RawValueBuffer struct { + *bytes.Reader +} + +type rawHiveCache struct { + mu sync.Mutex + + // Maintain a cache of already parsed hives + hive_cache map[string]*regparser.Registry +} + +func (self *rawHiveCache) Get(name string) (*regparser.Registry, bool) { + self.mu.Lock() + defer self.mu.Unlock() + + res, ok := self.hive_cache[name] + return res, ok +} + +func (self *rawHiveCache) Set(name string, reg *regparser.Registry) { + self.mu.Lock() + defer self.mu.Unlock() + + self.hive_cache[name] = reg +} + +type RawRegFileSystemAccessor struct { + scope vfilter.Scope + root *accessors.OSPath + + cache *RawRegFileSystemAccessorCache +} + +// Registery filesystems are usually case insensitive. +func (self RawRegFileSystemAccessor) GetCanonicalFilename( + path *accessors.OSPath) string { + return strings.ToLower(path.String()) +} + +func getRegHiveCache(scope vfilter.Scope) *rawHiveCache { + result_any := vql_subsystem.CacheGet(scope, RawRegFileSystemTag) + if result_any != nil { + cached, ok := result_any.(*rawHiveCache) + if ok { + return cached + } + } + + result := &rawHiveCache{ + hive_cache: make(map[string]*regparser.Registry), + } + vql_subsystem.CacheSet(scope, RawRegFileSystemTag, result) + + return result +} + +func getRegHive(scope vfilter.Scope, + file_path *accessors.OSPath) (*regparser.Registry, error) { + + // Cache the parsed hive under the underlying file. + pathspec := file_path.PathSpec() + base_pathspec := accessors.PathSpec{ + DelegateAccessor: pathspec.DelegateAccessor, + DelegatePath: pathspec.GetDelegatePath(), + } + cache_key := base_pathspec.String() + + hive_cache := getRegHiveCache(scope) + reg, pres := hive_cache.Get(cache_key) + if pres { + return reg, nil + } + + lru_size := vql_subsystem.GetIntFromRow( + scope, scope, constants.RAW_REG_CACHE_SIZE) + + delegate, err := file_path.Delegate(scope) + if err != nil { + return nil, err + } + + paged_reader, err := readers.NewAccessorReader( + scope, pathspec.DelegateAccessor, delegate, int(lru_size)) + if err != nil { + scope.Log("%v: did you provide a Pathspec?", err) + return nil, err + } + + // Make sure we can read the header so we can propagate errors + // properly. + header := make([]byte, 4) + _, err = paged_reader.ReadAt(header, 0) + if err != nil { + paged_reader.Close() + return nil, err + } + + hive, err := regparser.NewRegistry(paged_reader) + if err != nil { + paged_reader.Close() + return nil, err + } + + hive_cache.Set(cache_key, hive) + + return hive, nil +} + +const RawRegFileSystemTag = "_RawReg" + +func (self RawRegFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "raw_reg", + Description: `Access keys and values by parsing the raw registry hive. Path is a pathspec having delegate opening the raw registry hive.`, + } +} + +func (self *RawRegFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + return &RawRegFileSystemAccessor{ + scope: scope, + root: self.root, + cache: getRegFileSystemAccessorCache(scope), + }, nil +} + +// Raw Registry paths a just just generic paths: +// 1. Separator can be / or \ when specified. +// 2. Path are always serialized with / +// 3. No required hive at first element. +// 4. Paths start with / since they refer to the root of the raw hive file. +func (self RawRegFileSystemAccessor) ParsePath(path string) (*accessors.OSPath, error) { + return self.root.Parse(path) +} + +func (self *RawRegFileSystemAccessor) ReadDir(key_path string) ( + []accessors.FileInfo, error) { + + full_path, err := self.ParsePath(key_path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(full_path) +} + +func (self *RawRegFileSystemAccessor) ReadDirWithOSPath( + full_path *accessors.OSPath) (result []accessors.FileInfo, err error) { + + contents, _, err := self._readDirWithOSPath(full_path) + return contents, err +} + +// Return all the contents in the directory including all keys and all +// values, even if some keys have a default value. +// Additionally returns the CM_KEY_NODE for this actual directory. + +// This function is recursive! It ascends to the root cell recursively +// and resolves all keys along the path to the required key. On each +// level the function tries the LRU to avoid further recursion. This +// means that in practice most of the time we wont actually be +// recursing more than a few levels because top level keys will be +// cached in the LRU. +func (self *RawRegFileSystemAccessor) _readDirWithOSPath( + full_path *accessors.OSPath) (result []accessors.FileInfo, key *regparser.CM_KEY_NODE, err error) { + + cache_key := full_path.String() + cached, err := self.cache.readdir_lru.Get(cache_key) + if err == nil { + cached_res, ok := cached.(*readDirLRUItem) + if ok { + metricsReadDirLruHit.Inc() + return cached_res.children, cached_res.key, cached_res.err + } + } + metricsReadDirLruMiss.Inc() + + // Cache the result of this function + defer func() { + err1 := self.cache.readdir_lru.Set(cache_key, &readDirLRUItem{ + children: result, + err: err, + key: key, + }) + if err1 != nil && err == nil { + err = err1 + } + }() + + // Listing the top level of the hive. + if len(full_path.Components) == 0 { + hive, err := getRegHive(self.scope, full_path) + if err != nil { + return nil, nil, err + } + + root_cell := hive.Profile.HCELL(hive.Reader, + 0x1000+int64(hive.BaseBlock.RootCell())) + + nk := root_cell.KeyNode() + if nk != nil { + listing, err := self._readDirFromKey(full_path, nk) + return listing, nk, err + } + return nil, nil, utils.NotFoundError + } + + parent := full_path.Dirname() + basename := full_path.Basename() + + // If the directory is not cached, get its parent and list it. + contents, key, err := self._readDirWithOSPath(parent) + if err != nil { + return nil, nil, err + } + + // Find the required key in the parent directory listing. + for _, item := range contents { + key, ok := item.(*RawRegKeyInfo) + if !ok { + continue + } + + // Found it! + if key._key != nil && + strings.EqualFold(key.Name(), basename) { + listing, err := self._readDirFromKey(full_path, key._key) + return listing, key._key, err + } + } + + return nil, nil, utils.NotFoundError +} + +func (self *RawRegFileSystemAccessor) _readDirFromKey( + parent *accessors.OSPath, key *regparser.CM_KEY_NODE) ( + result []accessors.FileInfo, err error) { + + subkeys := key.Subkeys() + for _, subkey := range subkeys { + basename := subkey.Name() + subkey := &RawRegKeyInfo{ + _full_path: parent.Append(basename), + _modtime: subkey.LastWriteTime().Time, + _key: subkey, + } + result = append(result, subkey) + } + + // All Values carry their mode time as the parent key + key_mod_time := key.LastWriteTime().Time + for _, value := range key.Values() { + basename := value.ValueName() + if basename == "" { + basename = "@" + } + value_obj := &RawRegValueInfo{ + RawRegKeyInfo: &RawRegKeyInfo{ + _full_path: parent.Append(basename), + _modtime: key_mod_time, + }, + _value: value, + } + result = append(result, value_obj) + } + return result, nil +} + +func (self *RawRegFileSystemAccessor) Open(path string) ( + accessors.ReadSeekCloser, error) { + stat, err := self.Lstat(path) + if err != nil { + return nil, err + } + + value_info, ok := stat.(*RawRegValueInfo) + if ok { + return NewValueBuffer( + value_info._value.ValueData().Data, stat), nil + } + + // Keys do not have any data. + serialized, _ := json.Marshal(stat.Data) + return NewValueBuffer(serialized, stat), nil +} + +func (self *RawRegFileSystemAccessor) OpenWithOSPath(path *accessors.OSPath) ( + accessors.ReadSeekCloser, error) { + stats, err := self.multiLstat(path) + if err != nil { + return nil, err + } + + // We are looking for a value to open try to find one but if now, + // just serialize the key data. + for _, stat := range stats { + value_info, ok := stat.(*RawRegValueInfo) + if ok { + return NewValueBuffer( + value_info._value.ValueData().Data, stat), nil + } + } + + // Keys do not have any data. + serialized, _ := json.Marshal(stats[0].Data) + return NewValueBuffer(serialized, stats[0]), nil +} + +func (self *RawRegFileSystemAccessor) Lstat(filename string) ( + accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +func (self *RawRegFileSystemAccessor) LstatWithOSPath( + full_path *accessors.OSPath) ( + accessors.FileInfo, error) { + + // Top level stat + if len(full_path.Components) == 0 { + return &accessors.VirtualFileInfo{ + Path: full_path, + IsDir_: true, + }, nil + } + + res, err := self.multiLstat(full_path) + if err != nil { + return nil, err + } + + // Return the first one. + return res[0], nil +} + +// The registry can have keys and values named the same so an Lstat +// can actually return two separate entities. This function returns +// both. +func (self *RawRegFileSystemAccessor) multiLstat( + full_path *accessors.OSPath) (res []accessors.FileInfo, err error) { + + name := full_path.Basename() + container := full_path.Dirname() + + children, err := self.ReadDirWithOSPath(container) + if err != nil { + return nil, err + } + + for _, child := range children { + child_name := child.Name() + + // Fetch default value as either @ or "" + if strings.EqualFold(child_name, name) || + (name == "@" && child_name == "") { + res = append(res, child) + } + } + + if len(res) == 0 { + return nil, errors.New("Key not found") + } + + return res, nil +} + +func init() { + accessors.Register(&RawRegFileSystemAccessor{ + root: accessors.MustNewGenericOSPathWithBackslashSeparator(""), + }) + + json.RegisterCustomEncoder(&RawRegKeyInfo{}, accessors.MarshalGlobFileInfo) + json.RegisterCustomEncoder(&RawRegValueInfo{}, accessors.MarshalGlobFileInfo) +} diff --git a/accessors/raw_registry/raw_registry_test.go b/accessors/raw_registry/raw_registry_test.go new file mode 100644 index 000000000..5be5b8c49 --- /dev/null +++ b/accessors/raw_registry/raw_registry_test.go @@ -0,0 +1,81 @@ +package raw_registry + +import ( + "context" + "path/filepath" + "sort" + "strings" + "testing" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/config" + "www.velocidex.com/golang/velociraptor/glob" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/logging" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" + "www.velocidex.com/golang/vfilter" + + _ "www.velocidex.com/golang/velociraptor/accessors/file" + _ "www.velocidex.com/golang/velociraptor/accessors/ntfs" +) + +func TestAccessorRawReg(t *testing.T) { + config_obj := config.GetDefaultConfig() + scope := vql_subsystem.MakeScope() + scope.SetLogger(logging.NewPlainLogger( + config_obj, &logging.FrontendComponent)) + + runtest := func(scope vfilter.Scope) ([]string, error) { + reg_accessor, err := accessors.GetAccessor("raw_reg", scope) + if err != nil { + return nil, err + } + + abs_path, _ := filepath.Abs("../../artifacts/testdata/files/SAM") + root := &accessors.PathSpec{ + DelegateAccessor: "file", + DelegatePath: abs_path, + } + root_path, err := accessors.NewWindowsOSPath(root.String()) + assert.NoError(t, err) + + globber := glob.NewGlobber() + defer globber.Close() + + glob_path, err := accessors.NewLinuxOSPath("/SAM/Domains/*/*") + assert.NoError(t, err) + + globber.Add(glob_path) + + hits := []string{} + for hit := range globber.ExpandWithContext( + context.Background(), scope, config_obj, root_path, reg_accessor) { + hits = append(hits, hit.OSPath().Path()) + } + + sort.Strings(hits) + return hits, nil + } + + // Check the logs - permission should be denied. + logging.ClearMemoryLogs() + + _, err := runtest(scope) + assert.NoError(t, err) + + assert.Contains(t, strings.Join(logging.GetMemoryLogs(), ""), + "Permission denied: [FILESYSTEM_READ]") + + // Now repeat with proper access + scope = vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{})) + + hits, err := runtest(scope) + assert.NoError(t, err) + + goldie.Assert(t, "TestAccessorRawReg", json.MustMarshalIndent(hits)) +} diff --git a/accessors/raw_registry/value_buffer.go b/accessors/raw_registry/value_buffer.go new file mode 100644 index 000000000..2662c5edf --- /dev/null +++ b/accessors/raw_registry/value_buffer.go @@ -0,0 +1,28 @@ +package raw_registry + +import ( + "bytes" + "io" + + "www.velocidex.com/golang/velociraptor/accessors" +) + +type ValueBuffer struct { + io.ReadSeeker + info accessors.FileInfo +} + +func (self *ValueBuffer) Stat() (accessors.FileInfo, error) { + return self.info, nil +} + +func (self *ValueBuffer) Close() error { + return nil +} + +func NewValueBuffer(buf []byte, stat accessors.FileInfo) *ValueBuffer { + return &ValueBuffer{ + bytes.NewReader(buf), + stat, + } +} diff --git a/accessors/registry/cache.go b/accessors/registry/cache.go new file mode 100644 index 000000000..f99e19201 --- /dev/null +++ b/accessors/registry/cache.go @@ -0,0 +1,125 @@ +//go:build windows +// +build windows + +package registry + +import ( + "time" + + "github.com/Velocidex/ttlcache/v2" + "www.velocidex.com/golang/velociraptor/constants" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +const ( + CACHE_TAG = "_REG_CACHE" +) + +type RegFileSystemAccessorCache struct { + lru *ttlcache.Cache + readdir_lru *ttlcache.Cache +} + +func (self *RegFileSystemAccessorCache) GetDir(key string) (*readDirLRUItem, bool) { + if self.readdir_lru == nil { + return nil, false + } + + cached, err := self.readdir_lru.Get(key) + if err == nil { + cached_res, ok := cached.(*readDirLRUItem) + if ok { + metricsReadDirLruHit.Inc() + return cached_res, true + } + } + metricsReadDirLruMiss.Inc() + + return nil, false +} + +func (self *RegFileSystemAccessorCache) SetDir(key string, dir *readDirLRUItem) { + if self.readdir_lru != nil { + self.readdir_lru.Set(key, dir) + } +} + +func (self *RegFileSystemAccessorCache) Get(key string) (*RegKeyInfo, bool) { + if self.lru == nil { + return nil, false + } + + cached, err := self.lru.Get(key) + if err == nil { + res, ok := cached.(*RegKeyInfo) + if ok { + metricsLruHit.Inc() + return res, true + } + } + + metricsLruMiss.Inc() + return nil, false +} + +func (self *RegFileSystemAccessorCache) Set(key string, value *RegKeyInfo) { + if self.lru != nil { + self.lru.Set(key, value) + } +} + +func (self *RegFileSystemAccessorCache) Close() { + self.lru.Close() + self.readdir_lru.Close() +} + +func getRegFileSystemAccessorCache(scope vfilter.Scope) *RegFileSystemAccessorCache { + cache, ok := vql_subsystem.CacheGet(scope, CACHE_TAG).(*RegFileSystemAccessorCache) + if ok { + return cache + } + + cache_size := int(vql_subsystem.GetIntFromRow( + scope, scope, constants.REG_CACHE_SIZE)) + if cache_size == 0 { + cache_size = 1000 + } + + // Cache is disabled. + if cache_size < 0 { + return &RegFileSystemAccessorCache{} + } + + cache_time := vql_subsystem.GetIntFromRow( + scope, scope, constants.REG_CACHE_TIME) + if cache_time == 0 { + cache_time = 10 + } + + cache = &RegFileSystemAccessorCache{ + lru: ttlcache.NewCache(), + readdir_lru: ttlcache.NewCache(), + } + + cache.lru.SetCacheSizeLimit(cache_size) + cache.lru.SetTTL(time.Second * time.Duration(cache_time)) + cache.lru.SkipTTLExtensionOnHit(true) + + cache.readdir_lru.SetCacheSizeLimit(cache_size) + cache.readdir_lru.SetTTL(time.Second * time.Duration(cache_time)) + cache.readdir_lru.SkipTTLExtensionOnHit(true) + + // Add the cache to the root scope so it can be visible outside + // our scope. This should maximize cache hits + root_scope := vql_subsystem.GetRootScope(scope) + + root_scope.AddDestructor(func() { + cache.Close() + cache.lru.Close() + cache.readdir_lru.Close() + }) + vql_subsystem.CacheSet(root_scope, CACHE_TAG, cache) + + return cache +} diff --git a/accessors/registry/doc.go b/accessors/registry/doc.go new file mode 100644 index 000000000..af7ad18a0 --- /dev/null +++ b/accessors/registry/doc.go @@ -0,0 +1,3 @@ +// Accessor to make the registry available via OS API + +package registry diff --git a/accessors/registry/fixtures/TestRegistrtFilesystemAccessor.golden b/accessors/registry/fixtures/TestRegistrtFilesystemAccessor.golden new file mode 100755 index 000000000..137124cb8 --- /dev/null +++ b/accessors/registry/fixtures/TestRegistrtFilesystemAccessor.golden @@ -0,0 +1,14 @@ +{ + "Root listing": [ + "HKEY_CLASSES_ROOT - drwxr-xr-x {\"type\":\"hive\"}", + "HKEY_CURRENT_USER - drwxr-xr-x {\"type\":\"hive\"}", + "HKEY_LOCAL_MACHINE - drwxr-xr-x {\"type\":\"hive\"}", + "HKEY_USERS - drwxr-xr-x {\"type\":\"hive\"}", + "HKEY_CURRENT_CONFIG - drwxr-xr-x {\"type\":\"hive\"}", + "HKEY_PERFORMANCE_DATA - drwxr-xr-x {\"type\":\"hive\"}" + ], + "Deep key": [ + "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\CMF\\LatestIndex - drwxr-xr-x {\"type\":\"key\"}", + "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\CMF\\CompressedSegments - -rwxr-xr-x {\"type\":\"DWORD\",\"value\":1}" + ] +} \ No newline at end of file diff --git a/accessors/registry/lru.go b/accessors/registry/lru.go new file mode 100644 index 000000000..6120fab7e --- /dev/null +++ b/accessors/registry/lru.go @@ -0,0 +1,16 @@ +//go:build windows +// +build windows + +package registry + +import ( + "time" + + "www.velocidex.com/golang/velociraptor/accessors" +) + +type readDirLRUItem struct { + children []accessors.FileInfo + err error + age time.Time +} diff --git a/accessors/registry/registry_windows.go b/accessors/registry/registry_windows.go new file mode 100644 index 000000000..2f32c4e96 --- /dev/null +++ b/accessors/registry/registry_windows.go @@ -0,0 +1,732 @@ +//go:build windows +// +build windows + +/* + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +// A filesystem accessor for accessing the windows registry. + +// We make the registry look like a filesystem: +// 1. Keys are mapped as directories, and values are files. +// 2. Map the root path to a virtual directory containing all the root keys. +// 3. Normalized paths contain / for directory separators. +// 4. Normalized paths have reg: prefix. +package registry + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/Velocidex/ordereddict" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "golang.org/x/sys/windows/registry" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +var ( + // Include some common aliases. + root_keys = ordereddict.NewDict(). // map[string]registry.Key{ + Set("HKEY_CLASSES_ROOT", registry.CLASSES_ROOT). + Set("HKEY_CURRENT_USER", registry.CURRENT_USER). + Set("HKEY_LOCAL_MACHINE", registry.LOCAL_MACHINE). + Set("HKEY_USERS", registry.USERS). + Set("HKEY_CURRENT_CONFIG", registry.CURRENT_CONFIG). + Set("HKEY_PERFORMANCE_DATA", registry.PERFORMANCE_DATA) + + // Values smaller than this will be included in the stat entry + // itself. + MAX_EMBEDDED_REG_VALUE = 4 * 1024 + + metricsReadValue = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "registry_getvalue", + Help: "Number of time we Queried Value from the registry", + }) + + metricsAccessorReadValue = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "registry_accessor_getvalue", + Help: "Number of time we Queried Value from the accessor", + }) + + metricsLruHit = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "registry_keyinfo_lru_hit", + Help: "Performance of the Key Info Cache", + }) + + metricsLruMiss = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "registry_keyinfo_lru_miss", + Help: "Performance of the Key Info Cache", + }) + + metricsReadDirLruHit = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "registry_readdir_lru_hit", + Help: "Performance of the Read Dir Cache", + }) + + metricsReadDirLruMiss = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "registry_readdir_lru_miss", + Help: "Performance of the Read Dir Cache", + }) + + metricsOpen = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "registry_open", + Help: "Total number of Open operations", + }) + + metricsStat = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "registry_stat", + Help: "Total number of Lstat operations", + }) + + metricsOpenKey = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "registry_openkey", + Help: "Total number of RegOpenKey operations", + }) +) + +func GetHiveFromName(name string) (registry.Key, bool) { + hive, pres := root_keys.Get(name) + if pres { + return hive.(registry.Key), pres + } + return registry.CLASSES_ROOT, false +} + +type RegKeyInfo struct { + _modtime time.Time + _full_path *accessors.OSPath + _data *ordereddict.Dict +} + +func (self *RegKeyInfo) IsDir() bool { + return true +} + +func (self *RegKeyInfo) Data() *ordereddict.Dict { + if self._data == nil { + return ordereddict.NewDict() + } + return self._data +} + +func (self *RegKeyInfo) Size() int64 { + return 0 +} + +func (self *RegKeyInfo) UniqueName() string { + // Key names can not have \ in them so it is safe to add this + // without risk of collisions. + return self._full_path.String() + "\\" +} + +func (self *RegKeyInfo) FullPath() string { + return self._full_path.String() +} + +func (self *RegKeyInfo) OSPath() *accessors.OSPath { + return self._full_path.Copy() +} + +func (self *RegKeyInfo) Mode() os.FileMode { + return 0755 | os.ModeDir +} + +func (self *RegKeyInfo) Name() string { + return self._full_path.Basename() +} + +func (self *RegKeyInfo) ModTime() time.Time { + return self._modtime +} + +func (self *RegKeyInfo) Mtime() time.Time { + return self.ModTime() +} + +func (self *RegKeyInfo) Btime() time.Time { + return self.Mtime() +} + +func (self *RegKeyInfo) Ctime() time.Time { + return self.Mtime() +} + +func (self *RegKeyInfo) Atime() time.Time { + return self.Mtime() +} + +// Not supported +func (self *RegKeyInfo) IsLink() bool { + return false +} + +func (self *RegKeyInfo) GetLink() (*accessors.OSPath, error) { + return nil, errors.New("Not implemented") +} + +func (u *RegKeyInfo) UnmarshalJSON(data []byte) error { + return nil +} + +type RegValueInfo struct { + RegKeyInfo + Type string + _size int64 + + // A private copy of the value data. This is not made + // available to VQL. The data made available to VQL will be + // attached to the Data field of the FileInfo struct. While + // that can only contain fields smaller than + // MAX_EMBEDDED_REG_VALUE, we store the full value in the + // _binary_data field. We can then return the buffer for an + // Open() call. + _binary_data []byte +} + +func (self *RegValueInfo) IsDir() bool { + return false +} + +func (self *RegValueInfo) UniqueName() string { + return self._full_path.String() +} + +func (self *RegValueInfo) Mode() os.FileMode { + return 0755 +} + +func (self *RegValueInfo) Data() *ordereddict.Dict { + metricsAccessorReadValue.Inc() + + self.materialize() + return self._data +} + +func (self *RegValueInfo) Size() int64 { + self.materialize() + + return self._size +} + +// RegValueInfo are lazy structures that only materialize themselves +// just in time. +func (self *RegValueInfo) materialize() error { + // Use self._data as indicator if the structure is materialized. + if self._data != nil { + return nil + } + + // Last component is the value name + value_name := self._full_path.Basename() + full_key_path := self._full_path.Dirname() + + // Internally we represent the default value of a key as the name + // '@' + if value_name == "@" { + value_name = "" + } + + hive, key_path, err := getHiveAndKey(full_key_path) + if err != nil { + // Cache the error + self._data = ordereddict.NewDict().Set("Error", err.Error()) + return err + } + + metricsOpenKey.Inc() + key, err := registry.OpenKey(hive, key_path, + registry.READ|registry.QUERY_VALUE|registry.WOW64_64KEY) + if err != nil { + // Cache the error + self._data = ordereddict.NewDict().Set("Error", err.Error()) + return err + } + defer key.Close() + + buf_size, value_type, value, err := getValue(key, value_name) + if err != nil { + // Cache the error + self._data = ordereddict.NewDict().Set("Error", err.Error()) + return err + } + + self._size = int64(buf_size) + + switch value_type { + case registry.DWORD, registry.DWORD_BIG_ENDIAN, registry.QWORD: + switch value_type { + case registry.DWORD_BIG_ENDIAN: + self.Type = "DWORD_BIG_ENDIAN" + + case registry.DWORD: + self.Type = "DWORD" + + case registry.QWORD: + self.Type = "QWORD" + } + + self._data = ordereddict.NewDict(). + Set("type", self.Type). + Set("value", value) + + case registry.BINARY: + if buf_size < MAX_EMBEDDED_REG_VALUE { + self._data = ordereddict.NewDict(). + Set("type", "BINARY"). + Set("value", value) + } + value_bytes, _ := value.([]byte) + self._binary_data = value_bytes + self.Type = "BINARY" + + case registry.MULTI_SZ: + self._binary_data, _ = json.Marshal(value) + self.Type = "MULTI_SZ" + + if buf_size < MAX_EMBEDDED_REG_VALUE { + self._data = ordereddict.NewDict(). + Set("type", "MULTI_SZ"). + Set("value", value) + } + + case registry.SZ, registry.EXPAND_SZ: + switch value_type { + case registry.SZ: + self.Type = "SZ" + + case registry.EXPAND_SZ: + self.Type = "EXPAND_SZ" + } + + value_str, _ := value.(string) + self._binary_data = []byte(value_str) + + if buf_size < MAX_EMBEDDED_REG_VALUE { + self._data = ordereddict.NewDict(). + Set("type", self.Type). + + // We do not expand the value data because this will + // depend on the agent's own environment strings. + Set("value", value) + } + + default: + value_bytes, _ := value.([]byte) + self._binary_data = value_bytes + self.Type = fmt.Sprintf("%d", value_type) + + if buf_size < MAX_EMBEDDED_REG_VALUE { + self._data = ordereddict.NewDict(). + Set("type", self.Type). + Set("value", value) + } else { + self._data = ordereddict.NewDict(). + Set("type", self.Type). + Set("value", "") + } + } + + return nil +} + +type ValueBuffer struct { + io.ReadSeeker + info accessors.FileInfo +} + +func (self *ValueBuffer) Stat() (accessors.FileInfo, error) { + return self.info, nil +} + +func (self *ValueBuffer) Close() error { + return nil +} + +func NewValueBuffer(buf []byte, stat accessors.FileInfo) *ValueBuffer { + return &ValueBuffer{ + bytes.NewReader(buf), + stat, + } +} + +type RegFileSystemAccessor struct { + cache *RegFileSystemAccessorCache +} + +func (self RegFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "registry", + Description: `Access the registery like a filesystem using the OS APIs.`, + } +} + +// Registery filesystems are usually case insensitive. +func (self RegFileSystemAccessor) GetCanonicalFilename( + path *accessors.OSPath) string { + return strings.ToLower(path.String()) +} + +func (self *RegFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + return &RegFileSystemAccessor{ + cache: getRegFileSystemAccessorCache(scope), + }, nil +} + +func (self RegFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewWindowsRegistryPath(path) +} + +func (self RegFileSystemAccessor) ReadDir(path string) ( + []accessors.FileInfo, error) { + + full_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(full_path) +} + +func (self RegFileSystemAccessor) ReadDirWithOSPath( + full_path *accessors.OSPath) (result []accessors.FileInfo, err error) { + + cache_key := full_path.String() + cached, ok := self.cache.GetDir(cache_key) + if ok { + return cached.children, cached.err + } + + // Cache the result of this function + defer func() { + self.cache.SetDir(cache_key, &readDirLRUItem{ + children: result, + err: err, + age: utils.GetTime().Now(), + }) + }() + + // Root directory is just the name of the hives. + if len(full_path.Components) == 0 { + for _, k := range root_keys.Keys() { + result = append(result, &accessors.VirtualFileInfo{ + IsDir_: true, + Path: full_path.Append(k), + Data_: ordereddict.NewDict(). + Set("type", "hive"), + }) + } + return result, nil + } + + hive, key_path, err := getHiveAndKey(full_path) + if err != nil { + return nil, err + } + + metricsOpenKey.Inc() + key, err := registry.OpenKey(hive, key_path, + registry.READ|registry.QUERY_VALUE| + registry.ENUMERATE_SUB_KEYS|registry.WOW64_64KEY) + if err != nil { + return nil, err + } + defer key.Close() + + // Now enumerate the subkeys + subkeys, err := key.ReadSubKeyNames(-1) + if err != nil { + return nil, err + } + + for _, subkey_name := range subkeys { + key_info, ok := self.cache.Get(full_path.Append(subkey_name).String()) + if ok { + result = append(result, key_info) + continue + } + + // Not in cache, we need to add it + metricsOpenKey.Inc() + subkey, err := registry.OpenKey(key, subkey_name, + registry.READ|registry.QUERY_VALUE| + registry.ENUMERATE_SUB_KEYS| + registry.WOW64_64KEY) + if err != nil { + continue + } + + // Add to the LRU + key_info, err = self.buildAndCacheKeyInfo( + subkey, full_path.Append(subkey_name)) + if err == nil { + result = append(result, key_info) + } + subkey.Close() + } + + // Now enumerate the values. + values, err := ReadValueNames(key) + if err != nil { + return nil, err + } + + if len(values) > 0 { + cached, ok := self.cache.Get(full_path.String()) + if !ok { + cached, _ = self.buildAndCacheKeyInfo(key, full_path) + } + + for _, value_name := range values { + if value_name == "" { + value_name = "@" + } + value_info, err := getValueInfo( + cached.ModTime(), + full_path.Append(value_name)) + if err != nil { + continue + } + result = append(result, value_info) + } + } + + return result, nil +} + +func (self RegFileSystemAccessor) OpenWithOSPath(path *accessors.OSPath) ( + accessors.ReadSeekCloser, error) { + + metricsOpen.Inc() + + // Try to open the key as a value + value_info, err := self.lstatValue(path) + if err == nil { + value_info.materialize() + return NewValueBuffer(value_info._binary_data, value_info), nil + } + + // Did we just open a key? + stat, err := self.lstatKey(path) + if err != nil { + return nil, err + } + + // Keys do not have any data so just include the Data as a json blob. + serialized, _ := json.Marshal(stat.Data) + return NewValueBuffer(serialized, stat), nil +} + +func (self RegFileSystemAccessor) Open(path string) ( + accessors.ReadSeekCloser, error) { + stat, err := self.Lstat(path) + if err != nil { + return nil, err + } + + value_info, ok := stat.(*RegValueInfo) + if ok { + value_info.materialize() + return NewValueBuffer(value_info._binary_data, stat), nil + } + + // Keys do not have any data. + return NewValueBuffer([]byte{}, stat), nil +} + +func (self *RegFileSystemAccessor) Lstat(filename string) ( + accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +// Try to get the path as a key. +func (self *RegFileSystemAccessor) lstatKey( + full_path *accessors.OSPath) (*RegKeyInfo, error) { + + cached, ok := self.cache.Get(full_path.String()) + if ok { + return cached, nil + } + + hive, hive_key_path, err := getHiveAndKey(full_path) + if err != nil { + return nil, err + } + + metricsOpenKey.Inc() + key, err := registry.OpenKey(hive, hive_key_path, + registry.READ|registry.QUERY_VALUE|registry.WOW64_64KEY) + if err != nil { + return nil, err + } + defer key.Close() + + // We opened the full_path as a key - cache it for next time. + return self.buildAndCacheKeyInfo(key, full_path) +} + +// Try to get the path as a key. +func (self *RegFileSystemAccessor) lstatValue( + full_path *accessors.OSPath) (*RegValueInfo, error) { + // Try to open it as a value + if len(full_path.Components) == 0 { + return nil, utils.NotFoundError + } + + // Maybe its a value then - open the containing key + // and return a valueInfo + containing_key := full_path.Dirname() + + // We have the containing key in cache - use it. + cached, ok := self.cache.Get(containing_key.String()) + if ok { + return getValueInfo(cached.ModTime(), full_path) + } + + // We need to open the containing key + hive, hive_key_path, err := getHiveAndKey(containing_key) + if err != nil { + return nil, err + } + + metricsOpenKey.Inc() + key, err := registry.OpenKey(hive, hive_key_path, + registry.READ|registry.QUERY_VALUE|registry.WOW64_64KEY) + if err != nil { + return nil, err + } + defer key.Close() + + cached, err = self.buildAndCacheKeyInfo(key, containing_key) + if err != nil { + return nil, err + } + return getValueInfo(cached.ModTime(), full_path) +} + +func (self *RegFileSystemAccessor) LstatWithOSPath( + full_path *accessors.OSPath) (accessors.FileInfo, error) { + + metricsStat.Inc() + + // Is the full path a key ? + cached, ok := self.cache.Get(full_path.String()) + if ok { + return cached, nil + } + + // No: Try to open it as a key + res, err := self.lstatKey(full_path) + if err == nil { + return res, nil + } + + return self.lstatValue(full_path) +} + +func (self *RegFileSystemAccessor) buildAndCacheKeyInfo( + key registry.Key, full_path *accessors.OSPath) ( + *RegKeyInfo, error) { + + stat, err := key.Stat() + if err != nil { + return nil, err + } + + res := &RegKeyInfo{ + _modtime: stat.ModTime(), + _full_path: full_path.Copy(), + _data: ordereddict.NewDict().Set("type", "key"), + } + + cache_key := full_path.String() + self.cache.Set(cache_key, res) + return res, nil +} + +func getValueInfo( + key_modtime time.Time, + full_path *accessors.OSPath) (*RegValueInfo, error) { + + return &RegValueInfo{ + RegKeyInfo: RegKeyInfo{ + // Values do not carry their own + // timestamp - the key they are in + // gets its timestamp updated whenever + // any of the values does so we just + // copy the key's timestamp to each + // value. + _modtime: key_modtime, + _full_path: full_path.Copy(), + _data: nil, // Not materialized yet - lazy + }}, nil +} + +func getHiveAndKey(full_path *accessors.OSPath) (registry.Key, string, error) { + if len(full_path.Components) == 0 { + return 0, "", errors.New("Invalid Path") + } + + hive_name := full_path.Components[0] + hive_any, pres := root_keys.Get(hive_name) + if !pres { + // Not a real hive + return 0, "", errors.New("Unknown hive") + } + + hive := hive_any.(registry.Key) + + // Produce a string to use on the OpenKey API - the key is joined + // with \ on all components after the hive. + key_path := "" + if len(full_path.Components) > 1 { + key_path = strings.Join(full_path.Components[1:], "\\") + } + + return hive, key_path, nil +} + +func init() { + accessors.Register(&RegFileSystemAccessor{}) + json.RegisterCustomEncoder(&RegKeyInfo{}, accessors.MarshalGlobFileInfo) + json.RegisterCustomEncoder(&RegValueInfo{}, accessors.MarshalGlobFileInfo) +} diff --git a/accessors/registry/registry_windows_test.go b/accessors/registry/registry_windows_test.go new file mode 100644 index 000000000..c7ec4f464 --- /dev/null +++ b/accessors/registry/registry_windows_test.go @@ -0,0 +1,47 @@ +//go:build windows +// +build windows + +package registry + +import ( + "fmt" + "regexp" + "testing" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/json" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" +) + +func TestRegistryFilesystemAccessor(t *testing.T) { + scope := vql_subsystem.MakeScope() + accessor, err := (&RegFileSystemAccessor{}).New(scope) + assert.NoError(t, err) + + ls := func(path string, filter string) []string { + filter_re := regexp.MustCompile(filter) + + children, err := accessor.ReadDir(path) + assert.NoError(t, err) + + results := []string{} + for _, c := range children { + path := fmt.Sprintf("%v - %v %v", c.FullPath(), + c.Mode(), c.Data()) + if filter != "" && !filter_re.MatchString(path) { + continue + } + results = append(results, path) + } + return results + } + + results := ordereddict.NewDict() + results.Set("Root listing", ls("/", ".")) + results.Set("Deep key", ls("HKLM/SYSTEM/CurrentControlSet/Control/CMF", "CompressedSegments|LatestIndex")) + + goldie.Assert(t, "TestRegistrtFilesystemAccessor", + json.MustMarshalIndent(results)) +} diff --git a/accessors/registry/values.go b/accessors/registry/values.go new file mode 100644 index 000000000..9c48c5404 --- /dev/null +++ b/accessors/registry/values.go @@ -0,0 +1,142 @@ +//go:build windows +// +build windows + +package registry + +import ( + "strings" + "sync" + "syscall" + "unicode/utf16" + "unsafe" + + "golang.org/x/sys/windows/registry" + "www.velocidex.com/golang/velociraptor/vql/windows" +) + +const ( + _ERROR_NO_MORE_ITEMS syscall.Errno = 259 +) + +var pool = sync.Pool{ + New: func() interface{} { + buffer := make([]byte, 1024) + return &buffer + }, +} + +// A more optimized key.GetValue() - avoid unnecessary syscalls and allocations +func getValue(key registry.Key, value_name string) ( + buf_size int, value_type uint32, value interface{}, err error) { + + metricsReadValue.Inc() + + // Use the pool to avoid allocations. + cached_buffer := pool.Get().(*[]byte) + defer pool.Put(cached_buffer) + + data := *cached_buffer + + buf_size, value_type, err = key.GetValue(value_name, data) + if err == syscall.ERROR_MORE_DATA { + + // Try again with larger buffer. + buf := make([]byte, buf_size) + buf_size, value_type, err = key.GetValue(value_name, buf) + if err != nil { + return buf_size, value_type, "", err + } + data = buf + } + if err != nil { + return buf_size, value_type, "", err + } + + // Now parse the value based on the type. + // Following code is based on https://cs.opensource.google/go/x/sys/+/refs/tags/v0.18.0:windows/registry/value.go + switch value_type { + case registry.DWORD: + if buf_size == 4 { + var val32 uint32 + copy((*[4]byte)(unsafe.Pointer(&val32))[:], data) + return buf_size, value_type, uint64(val32), nil + } + + case registry.QWORD: + if buf_size == 8 { + var val64 uint64 + copy((*[8]byte)(unsafe.Pointer(&val64))[:], data) + return buf_size, value_type, uint64(val64), nil + } + + case registry.BINARY: + // Need to make a copy of the data so the buffer may be + // returned to the pool. + new_buff := append([]byte{}, data[:buf_size]...) + return buf_size, value_type, new_buff, nil + + // We deliberately do not expand this because it depends on + // the process env. + case registry.SZ, registry.EXPAND_SZ: + u := (*[1 << 29]uint16)(unsafe.Pointer(&data[0]))[: len(data)/2 : len(data)/2] + return buf_size, value_type, syscall.UTF16ToString(u), nil + + case registry.MULTI_SZ: + u := (*[1 << 29]uint16)(unsafe.Pointer(&data[0]))[: len(data)/2 : len(data)/2] + parts := strings.Split(string(utf16.Decode(u)), "\x00") + res := []string{} + for _, p := range parts { + if p != "" { + res = append(res, p) + } + } + return buf_size, value_type, res, nil + + default: + } + + // Otherwise just return the binary buffer. + new_buff := append([]byte{}, data[:buf_size]...) + return buf_size, value_type, new_buff, nil +} + +func ReadValueNames(k registry.Key) ([]string, error) { + ki, err := k.Stat() + if err != nil { + return nil, err + } + + names := make([]string, 0, ki.ValueCount) + // Use the pool to avoid allocations. + cached_buffer := pool.Get().(*[]byte) + defer pool.Put(cached_buffer) + + buf := *cached_buffer + +loopItems: + for i := uint32(0); ; i++ { + l := uint32(len(buf)) / 2 + for { + err := windows.RegEnumValue(syscall.Handle(k), i, &buf[0], &l, nil, nil, nil, nil) + if err == nil { + break + } + if err == syscall.ERROR_MORE_DATA { + // Double buffer size and try again. + buf = make([]byte, 2*len(buf)) + l = uint32(len(buf)) / 2 + continue + } + + if err == _ERROR_NO_MORE_ITEMS { + break loopItems + } + + return names, err + } + + u := (*[1 << 29]uint16)(unsafe.Pointer(&buf[0]))[: len(buf)/2 : len(buf)/2] + names = append(names, syscall.UTF16ToString(u)) + } + return names, nil +} diff --git a/accessors/s3/docs.go b/accessors/s3/docs.go new file mode 100644 index 000000000..d9fead21c --- /dev/null +++ b/accessors/s3/docs.go @@ -0,0 +1,15 @@ +package s3 + +// This is an S3 accessor + +// Sample query: + +// LET S3_CREDENTIALS <= dict(endpoint='http://127.0.0.1:4566/', credentials_key='admin', credentials_secret='password', no_verify_cert=1) +// SELECT *, read_file(filename=OSPath, length=10, accessor='s3') AS Data FROM glob(globs='/velociraptor/orgs/root/clients/C.39a107c4c58c5efa/collections/*/uploads/auto/*', accessor='s3') + +// This accessor has two versions: + +// 1. The one built with the official aws client library is full +// featured but comes with an increased binary size. Build this +// version using the sumo option. +// 2. The default one is built using the leaner minio library. diff --git a/accessors/s3/file_info.go b/accessors/s3/file_info.go new file mode 100644 index 000000000..a68fa9bd4 --- /dev/null +++ b/accessors/s3/file_info.go @@ -0,0 +1,79 @@ +package s3 + +import ( + "errors" + "os" + "time" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" +) + +type S3FileInfo struct { + path *accessors.OSPath + is_dir bool + size int64 + mod_time time.Time +} + +func (self *S3FileInfo) IsDir() bool { + return self.is_dir +} + +func (self *S3FileInfo) Size() int64 { + return self.size +} + +func (self *S3FileInfo) Data() *ordereddict.Dict { + result := ordereddict.NewDict() + return result +} + +func (self *S3FileInfo) Name() string { + return self.path.Basename() +} + +func (self *S3FileInfo) Mode() os.FileMode { + var result os.FileMode = 0755 + if self.IsDir() { + result |= os.ModeDir + } + return result +} + +func (self *S3FileInfo) ModTime() time.Time { + return self.mod_time +} + +func (self *S3FileInfo) FullPath() string { + return self.path.String() +} + +func (self *S3FileInfo) OSPath() *accessors.OSPath { + return self.path.Copy() +} + +func (self *S3FileInfo) Mtime() time.Time { + return self.mod_time +} + +func (self *S3FileInfo) Ctime() time.Time { + return self.Mtime() +} + +func (self *S3FileInfo) Btime() time.Time { + return self.Mtime() +} + +func (self *S3FileInfo) Atime() time.Time { + return self.Mtime() +} + +// Not supported +func (self *S3FileInfo) IsLink() bool { + return false +} + +func (self *S3FileInfo) GetLink() (*accessors.OSPath, error) { + return nil, errors.New("Not implemented") +} diff --git a/accessors/s3/reader.go b/accessors/s3/reader.go new file mode 100644 index 000000000..c500f6885 --- /dev/null +++ b/accessors/s3/reader.go @@ -0,0 +1,66 @@ +//go:build sumo +// +build sumo + +package s3 + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/aws/aws-sdk-go-v2/aws" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type S3Reader struct { + ctx context.Context + downloader *manager.Downloader + offset int64 + bucket string + key string +} + +func (self *S3Reader) Read(buff []byte) (int, error) { + to_read := int64(len(buff)) - 1 + + req := &s3.GetObjectInput{ + Bucket: aws.String(self.bucket), + Key: aws.String(self.key), + Range: aws.String( + fmt.Sprintf("bytes=%d-%d", self.offset, + self.offset+to_read)), + } + + n, err := self.downloader.Download(self.ctx, + manager.NewWriteAtBuffer(buff), req) + + if err != nil { + var re *awshttp.ResponseError + if errors.As(err, &re) { + if re.HTTPStatusCode() == 416 { + return 0, io.EOF + } + } + + return 0, err + } + self.offset += n + + if n < to_read { + return int(n), io.EOF + } + + return int(n), nil +} + +func (self *S3Reader) Seek(offset int64, whence int) (int64, error) { + self.offset = offset + return self.offset, nil +} + +func (self *S3Reader) Close() error { + return nil +} diff --git a/accessors/s3/s3.go b/accessors/s3/s3.go new file mode 100644 index 000000000..4befb5191 --- /dev/null +++ b/accessors/s3/s3.go @@ -0,0 +1,273 @@ +//go:build sumo +// +build sumo + +/* An accessor for an S3 bucket */ + +package s3 + +import ( + "context" + "strings" + "sync" + + "github.com/Velocidex/ordereddict" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +var ( + // Total number of keys we fetch in each ListObjects call + mu sync.Mutex + maxKeys = int32(1000) + + metricS3OpsListObjects = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "s3_ops_list_objects", + Help: "Number of s3 ListObjects operations", + }) +) + +type RawS3SystemAccessor struct { + ctx context.Context + scope vfilter.Scope +} + +func (self RawS3SystemAccessor) ParsePath(path string) (*accessors.OSPath, error) { + return accessors.NewLinuxOSPath(path) +} + +func (self RawS3SystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + result := &RawS3SystemAccessor{ + ctx: context.TODO(), + scope: scope, + } + return result, nil +} + +func (self RawS3SystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "s3", + Description: `Allows access to S3 buckets.`, + Permissions: []acls.ACL_PERMISSION{acls.NETWORK}, + ScopeVar: constants.S3_CREDENTIALS, + ArgType: S3AcccessorArgs{}, + } +} + +func (self RawS3SystemAccessor) ReadDir( + path string) ([]accessors.FileInfo, error) { + + parsed_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(parsed_path) +} + +func (self RawS3SystemAccessor) ReadDirWithOSPath( + path *accessors.OSPath) ([]accessors.FileInfo, error) { + + client, err := GetS3Client(self.ctx, self.scope) + if err != nil { + return nil, err + } + + if len(path.Components) == 0 { + resp, err := client.ListBuckets(self.ctx, &s3.ListBucketsInput{}) + if err != nil { + return nil, err + } + result := make([]accessors.FileInfo, 0, len(resp.Buckets)) + for _, b := range resp.Buckets { + result = append(result, &S3FileInfo{ + path: accessors.MustNewLinuxOSPath(*b.Name), + is_dir: true, + mod_time: *b.CreationDate, + }) + } + return result, nil + } + + bucket, key, err := getBucketAndKey(path) + if err != nil { + return nil, err + } + + bucket_path := accessors.MustNewLinuxOSPath(bucket) + child_directories := ordereddict.NewDict() + child_files := []*S3FileInfo{} + + params := &s3.ListObjectsV2Input{ + Bucket: aws.String(bucket), + Prefix: aws.String(key), + } + + // Create the Paginator for the ListObjectsV2 operation. + paginator := s3.NewListObjectsV2Paginator( + client, params, func(o *s3.ListObjectsV2PaginatorOptions) { + mu.Lock() + defer mu.Unlock() + + o.Limit = maxKeys + }) + + result := []accessors.FileInfo{} + for paginator.HasMorePages() { + metricS3OpsListObjects.Inc() + + page, err := paginator.NextPage(self.ctx) + if err != nil { + return nil, err + } + + for _, object := range page.Contents { + component_path, err := self.ParsePath(*object.Key) + if err != nil { + continue + } + + object_path := bucket_path.Append(component_path.Components...) + + // Skip components that are not direct children. + if len(object_path.Components) > len(path.Components)+1 { + child_directories.Set( + object_path.Components[len(path.Components)], true) + + } else if len(object_path.Components) == len(path.Components)+1 { + child_files = append(child_files, &S3FileInfo{ + path: object_path, + is_dir: false, + size: *object.Size, + mod_time: *object.LastModified, + }) + } + } + } + + for _, child_dir := range child_directories.Keys() { + result = append(result, &S3FileInfo{ + path: path.Append(child_dir), + is_dir: true, + }) + } + + for _, info := range child_files { + result = append(result, info) + } + + return result, nil +} + +func getBucketAndKey(path *accessors.OSPath) (string, string, error) { + if len(path.Components) == 0 { + return "", "", utils.NotFoundError + } + + bucket := path.Components[0] + components := append([]string{}, path.Components[1:]...) + key := strings.Join(components, "/") + + return bucket, key, nil +} + +func (self RawS3SystemAccessor) OpenWithOSPath( + path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + + svc, err := GetS3Client(self.ctx, self.scope) + if err != nil { + return nil, err + } + + bucket, key, err := getBucketAndKey(path) + if err != nil { + return nil, err + } + + reader := &S3Reader{ + ctx: self.ctx, + downloader: manager.NewDownloader(svc), + bucket: bucket, + key: key, + } + + // Wrap the reader in an in memory cache so we do not have many + // small reads from the network. + paged_reader, err := utils.NewPagedReader( + utils.MakeReaderAtter(reader), 1024*1024, 20) + return utils.NewReadSeekReaderAdapter(paged_reader, nil), err +} + +func (self RawS3SystemAccessor) Open( + filename string) (accessors.ReadSeekCloser, error) { + + parsed_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(parsed_path) +} + +func (self RawS3SystemAccessor) Lstat(path string) (accessors.FileInfo, error) { + + parsed_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(parsed_path) +} + +func (self RawS3SystemAccessor) LstatWithOSPath( + path *accessors.OSPath) (accessors.FileInfo, error) { + + svc, err := GetS3Client(self.ctx, self.scope) + if err != nil { + return nil, err + } + + bucket, key, err := getBucketAndKey(path) + if err != nil { + return nil, err + } + + headObj := s3.HeadObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + } + + result, err := svc.HeadObject(self.ctx, &headObj) + if err != nil { + return nil, err + } + + return &accessors.VirtualFileInfo{ + Data_: ordereddict.NewDict(), + Path: path, + Size_: *result.ContentLength, + }, nil +} + +func init() { + accessors.Register(&RawS3SystemAccessor{}) +} + +// Set the page size for tests. Normally we dont need to adjust this +// at all. Used in tests. +func SetPageSize(size int32) { + mu.Lock() + defer mu.Unlock() + + maxKeys = size +} diff --git a/accessors/s3/s3_minio.go b/accessors/s3/s3_minio.go new file mode 100644 index 000000000..d83ec973d --- /dev/null +++ b/accessors/s3/s3_minio.go @@ -0,0 +1,267 @@ +//go:build !sumo +// +build !sumo + +/* An accessor for an S3 bucket */ + +package s3 + +import ( + "context" + "strings" + "sync" + + "github.com/Velocidex/ordereddict" + "github.com/minio/minio-go/v7" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +var ( + // Total number of keys we fetch in each ListObjects call + mu sync.Mutex + maxKeys = 1000 + + metricS3OpsListObjects = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "s3_ops_list_objects", + Help: "Number of s3 ListObjects operations", + }) +) + +type RawS3SystemAccessor struct { + ctx context.Context + scope vfilter.Scope +} + +func (self RawS3SystemAccessor) ParsePath(path string) (*accessors.OSPath, error) { + return accessors.NewLinuxOSPath(path) +} + +func (self RawS3SystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + result := &RawS3SystemAccessor{ + ctx: context.TODO(), + scope: scope, + } + return result, nil +} + +func (self RawS3SystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "s3", + Description: `Allows access to S3 buckets.`, + Permissions: []acls.ACL_PERMISSION{acls.NETWORK}, + ScopeVar: constants.S3_CREDENTIALS, + ArgType: S3AcccessorArgs{}, + } +} + +func (self RawS3SystemAccessor) ReadDir( + path string) ([]accessors.FileInfo, error) { + + parsed_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(parsed_path) +} + +func (self RawS3SystemAccessor) ReadDirWithOSPath( + path *accessors.OSPath) ([]accessors.FileInfo, error) { + + s3Client, err := GetS3Client(self.ctx, self.scope) + if err != nil { + return nil, err + } + + if len(path.Components) == 0 { + metricS3OpsListObjects.Inc() + resp, err := s3Client.ListBuckets(self.ctx) + if err != nil { + return nil, err + } + result := make([]accessors.FileInfo, 0, len(resp)) + for _, b := range resp { + result = append(result, &S3FileInfo{ + path: accessors.MustNewLinuxOSPath(b.Name), + is_dir: true, + mod_time: b.CreationDate, + }) + } + return result, nil + } + + bucket, key, err := getBucketAndKey(path) + if err != nil { + return nil, err + } + + bucket_path := accessors.MustNewLinuxOSPath(bucket) + child_directories := ordereddict.NewDict() + child_files := []*S3FileInfo{} + + opts := minio.ListObjectsOptions{ + Prefix: key, + MaxKeys: maxKeys, + } + + obj_chan := s3Client.ListObjects(self.ctx, bucket, opts) + +outer: + for { + select { + case <-self.ctx.Done(): + return nil, nil + + case object, ok := <-obj_chan: + if !ok { + break outer + } + + if object.Err != nil { + continue + } + + component_path, err := self.ParsePath(object.Key) + if err != nil { + continue + } + + object_path := bucket_path.Append(component_path.Components...) + + // Skip components that are not direct children. + if len(object_path.Components) > len(path.Components)+1 { + child_directories.Set( + object_path.Components[len(path.Components)], true) + + } else if len(object_path.Components) == len(path.Components)+1 { + child_files = append(child_files, &S3FileInfo{ + path: object_path, + is_dir: false, + size: object.Size, + mod_time: object.LastModified, + }) + } + } + } + + result := []accessors.FileInfo{} + for _, child_dir := range child_directories.Keys() { + result = append(result, &S3FileInfo{ + path: path.Append(child_dir), + is_dir: true, + }) + } + + for _, info := range child_files { + result = append(result, info) + } + + return result, nil +} + +func getBucketAndKey(path *accessors.OSPath) (string, string, error) { + if len(path.Components) == 0 { + return "", "", utils.NotFoundError + } + + bucket := path.Components[0] + components := append([]string{}, path.Components[1:]...) + key := strings.Join(components, "/") + + return bucket, key, nil +} + +func (self RawS3SystemAccessor) OpenWithOSPath( + path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + + s3Client, err := GetS3Client(self.ctx, self.scope) + if err != nil { + return nil, err + } + + bucket, key, err := getBucketAndKey(path) + if err != nil { + return nil, err + } + + reader, err := s3Client.GetObject( + self.ctx, bucket, key, minio.GetObjectOptions{}) + if err != nil { + return nil, err + } + + // Wrap the reader in an in memory cache so we do not have many + // small reads from the network. + paged_reader, err := utils.NewPagedReader( + utils.MakeReaderAtter(reader), 1024*1024, 20) + return utils.NewReadSeekReaderAdapter(paged_reader, nil), err +} + +func (self RawS3SystemAccessor) Open( + filename string) (accessors.ReadSeekCloser, error) { + + parsed_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(parsed_path) +} + +func (self RawS3SystemAccessor) Lstat(path string) (accessors.FileInfo, error) { + + parsed_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(parsed_path) +} + +func (self RawS3SystemAccessor) LstatWithOSPath( + path *accessors.OSPath) (accessors.FileInfo, error) { + + client, err := GetS3Client(self.ctx, self.scope) + if err != nil { + return nil, err + } + + bucket, key, err := getBucketAndKey(path) + if err != nil { + return nil, err + } + + stat_obj, err := client.StatObject(self.ctx, bucket, key, + minio.StatObjectOptions{}) + if err != nil { + return nil, err + } + + return &accessors.VirtualFileInfo{ + Data_: ordereddict.NewDict(), + Path: path, + Size_: stat_obj.Size, + Mtime_: stat_obj.LastModified, + }, nil +} + +func init() { + accessors.Register(&RawS3SystemAccessor{}) +} + +// Set the page size for tests. Normally we don't need to adjust this +// at all. Used in tests. +func SetPageSize(size int) { + mu.Lock() + defer mu.Unlock() + + maxKeys = size +} diff --git a/accessors/s3/session.go b/accessors/s3/session.go new file mode 100644 index 000000000..2c6465cc1 --- /dev/null +++ b/accessors/s3/session.go @@ -0,0 +1,176 @@ +//go:build sumo +// +build sumo + +package s3 + +import ( + "context" + "errors" + + "github.com/Velocidex/ordereddict" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + "www.velocidex.com/golang/velociraptor/artifacts" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/networking" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/arg_parser" + "www.velocidex.com/golang/vfilter/utils/dict" +) + +const ( + S3_TAG = "_S3_TAG" +) + +type S3AcccessorArgs struct { + Secret string `vfilter:"optional,field=secret,doc=The name of a secret to use."` + Region string `vfilter:"optional,field=region,doc=The region."` + CredentialsKey string `vfilter:"optional,field=credentials_key"` + CredentialsSecret string `vfilter:"optional,field=credentials_secret"` + CredentialsToken string `vfilter:"optional,field=credentials_token"` + Endpoint string `vfilter:"optional,field=endpoint"` + SkipVerify bool `vfilter:"optional,field=skip_verify"` +} + +func GetS3Client( + ctx context.Context, + scope vfilter.Scope) (res *s3.Client, err error) { + + // Empty credentials are OK - they just mean to get creds from the + // process env + setting, pres := scope.Resolve(constants.S3_CREDENTIALS) + if !pres { + setting = ordereddict.NewDict() + } + + args := dict.RowToDict(ctx, scope, setting) + arg := &S3AcccessorArgs{} + err = arg_parser.ExtractArgsWithContext(ctx, scope, args, arg) + if err != nil { + return nil, err + } + + err = maybeForceSecrets(ctx, scope, arg) + if err != nil { + return nil, err + } + + // Check for a secret from the secrets service + if arg.Secret != "" { + arg, err = getSecret(ctx, scope, arg.Secret) + if err != nil { + return nil, err + } + } + + conf := []func(*config.LoadOptions) error{} + if arg.Region != "" { + conf = append(conf, config.WithRegion(arg.Region)) + } + + if arg.CredentialsKey != "" && arg.CredentialsSecret != "" { + conf = append(conf, config.WithCredentialsProvider( + credentials.NewStaticCredentialsProvider( + arg.CredentialsKey, arg.CredentialsSecret, + arg.CredentialsToken), + )) + } + + s3_opts := []func(*s3.Options){} + if arg.Endpoint != "" { + s3_opts = append(s3_opts, func(o *s3.Options) { + o.BaseEndpoint = aws.String(arg.Endpoint) + }) + } + + clientConfig, ok := artifacts.GetConfig(scope) + if ok { + if arg.SkipVerify { + http_client, err := networking.GetSkipVerifyHTTPClient( + ctx, clientConfig, scope, "", nil) + if err != nil { + return nil, err + } + + conf = append(conf, config.WithHTTPClient(http_client)) + + } else { + http_client, err := networking.GetDefaultHTTPClient( + ctx, clientConfig, scope, "", nil) + if err != nil { + return nil, err + } + conf = append(conf, config.WithHTTPClient(http_client)) + } + } + + sess, err := config.LoadDefaultConfig(ctx, conf...) + if err != nil { + return nil, err + } + + client := s3.NewFromConfig(sess, s3_opts...) + + return client, nil +} + +func maybeForceSecrets( + ctx context.Context, scope vfilter.Scope, arg *S3AcccessorArgs) error { + + // Not running on the server, secrets dont work. + config_obj, ok := vql_subsystem.GetServerConfig(scope) + if !ok { + return nil + } + + if config_obj.Security == nil { + return nil + } + + if !config_obj.Security.VqlMustUseSecrets { + return nil + } + + // If an explicit secret is defined let it filter the URLs. + if arg.Secret != "" { + return nil + } + + return utils.SecretsEnforced +} + +func getSecret( + ctx context.Context, + scope vfilter.Scope, secret string) (*S3AcccessorArgs, error) { + config_obj, ok := vql_subsystem.GetServerConfig(scope) + if !ok { + return nil, errors.New("Secrets may only be used on the server") + } + + secrets_service, err := services.GetSecretsService(config_obj) + if err != nil { + return nil, err + } + + principal := vql_subsystem.GetPrincipal(scope) + secret_record, err := secrets_service.GetSecret(ctx, principal, + constants.AWS_S3_CREDS, secret) + if err != nil { + return nil, err + } + + arg := &S3AcccessorArgs{ + Region: secret_record.GetString("region"), + CredentialsKey: secret_record.GetString("credentials_key"), + CredentialsSecret: secret_record.GetString("credentials_secret"), + CredentialsToken: secret_record.GetString("credentials_token"), + Endpoint: secret_record.GetString("endpoint"), + SkipVerify: secret_record.GetBool("skip_verify"), + } + return arg, nil +} diff --git a/accessors/s3/session_minio.go b/accessors/s3/session_minio.go new file mode 100644 index 000000000..803dfbbfd --- /dev/null +++ b/accessors/s3/session_minio.go @@ -0,0 +1,131 @@ +//go:build !sumo +// +build !sumo + +package s3 + +import ( + "context" + "errors" + + "github.com/Velocidex/ordereddict" + "github.com/minio/minio-go/v7" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/tools" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/arg_parser" + "www.velocidex.com/golang/vfilter/utils/dict" +) + +const ( + S3_TAG = "_S3_TAG" +) + +type S3AcccessorArgs struct { + Secret string `vfilter:"optional,field=secret,doc=The name of a secret to use."` + Region string `vfilter:"optional,field=region,doc=The region."` + CredentialsKey string `vfilter:"optional,field=credentials_key"` + CredentialsSecret string `vfilter:"optional,field=credentials_secret"` + CredentialsToken string `vfilter:"optional,field=credentials_token"` + Endpoint string `vfilter:"optional,field=endpoint"` + SkipVerify bool `vfilter:"optional,field=skip_verify"` +} + +func GetS3Client( + ctx context.Context, + scope vfilter.Scope) (res *minio.Client, err error) { + + // Empty credentials are OK - they just mean to get creds from the + // process env + setting, pres := scope.Resolve(constants.S3_CREDENTIALS) + if !pres { + setting = ordereddict.NewDict() + } + + args := dict.RowToDict(ctx, scope, setting) + arg := &S3AcccessorArgs{} + err = arg_parser.ExtractArgsWithContext(ctx, scope, args, arg) + if err != nil { + return nil, err + } + + err = maybeForceSecrets(ctx, scope, arg) + if err != nil { + return nil, err + } + + // Check for a secret from the secrets service + if arg.Secret != "" { + arg, err = getSecret(ctx, scope, arg.Secret) + if err != nil { + return nil, err + } + } + + return tools.GetS3Client(ctx, scope, &tools.S3UploadArgs{ + Region: arg.Region, + CredentialsKey: arg.CredentialsKey, + CredentialsSecret: arg.CredentialsSecret, + CredentialsToken: arg.CredentialsToken, + Endpoint: arg.Endpoint, + SkipVerify: arg.SkipVerify, + }) +} + +func maybeForceSecrets( + ctx context.Context, scope vfilter.Scope, arg *S3AcccessorArgs) error { + + // Not running on the server, secrets dont work. + config_obj, ok := vql_subsystem.GetServerConfig(scope) + if !ok { + return nil + } + + if config_obj.Security == nil { + return nil + } + + if !config_obj.Security.VqlMustUseSecrets { + return nil + } + + // If an explicit secret is defined let it filter the URLs. + if arg.Secret != "" { + return nil + } + + return utils.SecretsEnforced +} + +func getSecret( + ctx context.Context, + scope vfilter.Scope, secret string) (*S3AcccessorArgs, error) { + config_obj, ok := vql_subsystem.GetServerConfig(scope) + if !ok { + return nil, errors.New("Secrets may only be used on the server") + } + + secrets_service, err := services.GetSecretsService(config_obj) + if err != nil { + return nil, err + } + + principal := vql_subsystem.GetPrincipal(scope) + secret_record, err := secrets_service.GetSecret(ctx, principal, + constants.AWS_S3_CREDS, secret) + if err != nil { + return nil, err + } + + arg := &S3AcccessorArgs{ + Region: secret_record.GetString("region"), + CredentialsKey: secret_record.GetString("credentials_key"), + CredentialsSecret: secret_record.GetString("credentials_secret"), + CredentialsToken: secret_record.GetString("credentials_token"), + Endpoint: secret_record.GetString("endpoint"), + SkipVerify: secret_record.GetBool("skip_verify"), + } + return arg, nil +} diff --git a/accessors/scope.go b/accessors/scope.go new file mode 100644 index 000000000..63ddc7437 --- /dev/null +++ b/accessors/scope.go @@ -0,0 +1 @@ +package accessors diff --git a/accessors/smb/cache.go b/accessors/smb/cache.go new file mode 100644 index 000000000..cbe85a372 --- /dev/null +++ b/accessors/smb/cache.go @@ -0,0 +1,217 @@ +package smb + +import ( + "context" + "fmt" + "net" + "strings" + "sync" + "time" + + "github.com/Velocidex/ttlcache/v2" + errors "github.com/go-errors/errors" + "github.com/hirochachacha/go-smb2" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "www.velocidex.com/golang/velociraptor/constants" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/arg_parser" + "www.velocidex.com/golang/vfilter/utils/dict" +) + +var ( + smbAccessorCurrentOpened = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "accessor_smb_current_open_files", + Help: "Number of currently opened files with the smb accessor.", + }) + + smbAccessorTotalRemoteMounts = promauto.NewCounter(prometheus.CounterOpts{ + Name: "accessor_smb_total_mounts", + Help: "Total Number of times the SMB accessor mounted a remote share", + }) + + smbAccessorCurrentRemoteMounts = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "accessor_smb_current_mounts", + Help: "Total Number of times the SMB accessor mounted a remote share", + }) +) + +type SMBConnectionContext struct { + mu sync.Mutex + err error + server string + conn net.Conn + session *smb2.Session + mount map[string]*smb2.Share +} + +func NewSMBConnectionContext( + ctx context.Context, scope vfilter.Scope, + server_name string) (*SMBConnectionContext, error) { + + creds, err := getCreadentials(ctx, scope, server_name) + if err != nil { + return nil, err + } + + if !strings.Contains(server_name, ":") { + server_name += ":445" + } + + conn, err := net.Dial("tcp", server_name) + if err != nil { + return nil, err + } + + d := &smb2.Dialer{ + Initiator: creds, + } + + session, err := d.Dial(conn) + if err != nil { + conn.Close() + return nil, err + } + + return &SMBConnectionContext{ + server: server_name, + conn: conn, + session: session, + mount: make(map[string]*smb2.Share), + }, nil +} + +func (self *SMBConnectionContext) Session() *smb2.Session { + return self.session +} + +func (self *SMBConnectionContext) Mount(name string) (*smb2.Share, error) { + share, pres := self.mount[name] + if pres { + return share, nil + } + + fs, err := self.session.Mount(name) + if err != nil { + return nil, err + } + self.mount[name] = fs + + smbAccessorTotalRemoteMounts.Inc() + smbAccessorCurrentRemoteMounts.Inc() + return fs, nil +} + +func (self *SMBConnectionContext) Close() { + if self.session != nil { + _ = self.session.Logoff() + } + if self.conn != nil { + self.conn.Close() + } + smbAccessorCurrentRemoteMounts.Sub(float64(len(self.mount))) +} + +type SMBMountCache struct { + mu sync.Mutex + ctx context.Context + scope vfilter.Scope + lru *ttlcache.Cache // map[server]*SMBConnectionContext +} + +func (self *SMBMountCache) GetHandle(server_name string) ( + *SMBConnectionContext, func(), error) { + self.mu.Lock() + defer self.mu.Unlock() + + cached_any, err := self.lru.Get(server_name) + if err == nil { + cached, ok := cached_any.(*SMBConnectionContext) + if ok { + cached.mu.Lock() + err := cached.err + if err != nil { + cached.mu.Unlock() + return nil, nil, err + } + return cached, cached.mu.Unlock, nil + } + } + + // Create a new context + cached, err := NewSMBConnectionContext(self.ctx, self.scope, server_name) + if err != nil { + // Cache the failure - this usually means wrong creds. + cached = &SMBConnectionContext{ + err: err, + } + } + + // Set to refresh the TTL + _ = self.lru.Set(server_name, cached) + cached.mu.Lock() + return cached, cached.mu.Unlock, err +} + +func NewSMBMountCache(scope vfilter.Scope) *SMBMountCache { + // Tie our lifetime to the root scope. + ctx, cancel := context.WithCancel(context.Background()) + result := &SMBMountCache{ + ctx: ctx, + scope: scope, + lru: ttlcache.NewCache(), + } + _ = result.lru.SetTTL(time.Hour) + result.lru.SetExpirationCallback( + func(key string, value interface{}) error { + ctx, ok := value.(*SMBConnectionContext) + if ok { + // Do not block the lru while closing. + go ctx.Close() + } + return nil + }) + + _ = vql_subsystem.GetRootScope(scope).AddDestructor(func() { + result.lru.Flush() + result.lru.Close() + cancel() + }) + return result +} + +func getCreadentials( + ctx context.Context, scope vfilter.Scope, hostname string) ( + *smb2.NTLMInitiator, error) { + + credentials, pres := scope.Resolve(constants.SMB_CREDENTIALS) + if !pres { + return nil, errors.New("No credentials provided for smb connections") + } + + args := dict.RowToDict(ctx, scope, credentials) + + var creds string + arg := &SMBAccessorArgs{} + err := arg_parser.ExtractArgsWithContext(ctx, scope, args, arg) + if err != nil { + // Try to support the old style args for backwards compatibility. + creds, pres = args.GetString(hostname) + } else { + creds, pres = arg.Hosts.GetString(hostname) + } + + if !pres { + return nil, fmt.Errorf("No credentials found for %v", hostname) + } + parts := strings.SplitN(creds, ":", 2) + if len(parts) < 2 { + return nil, fmt.Errorf("Invalid credentials provided for %v", hostname) + } + + return &smb2.NTLMInitiator{ + User: parts[0], + Password: parts[1], + }, nil +} diff --git a/accessors/smb/manipulator.go b/accessors/smb/manipulator.go new file mode 100644 index 000000000..2cfc4dc56 --- /dev/null +++ b/accessors/smb/manipulator.go @@ -0,0 +1,31 @@ +package smb + +import ( + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/utils" +) + +type SMBPathManipulator struct { + accessors.GenericPathManipulator +} + +func (self SMBPathManipulator) PathJoin(path *accessors.OSPath) string { + result := self.AsPathSpec(path) + + if result.GetDelegateAccessor() == "" && result.GetDelegatePath() == "" { + return result.Path + } + return result.String() +} + +func (self SMBPathManipulator) AsPathSpec(path *accessors.OSPath) *accessors.PathSpec { + result := &accessors.PathSpec{} + + // First component must lead with a \\ + if len(path.Components) > 0 { + result.Path = "\\" + utils.JoinComponents(path.Components, "\\") + } else { + result.Path = "" + } + return result +} diff --git a/accessors/smb/smb.go b/accessors/smb/smb.go new file mode 100644 index 000000000..3a0a7f545 --- /dev/null +++ b/accessors/smb/smb.go @@ -0,0 +1,280 @@ +package smb + +import ( + "fmt" + "io/fs" + "strings" + + "github.com/Velocidex/ordereddict" + errors "github.com/go-errors/errors" + "github.com/hirochachacha/go-smb2" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/constants" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +const ( + SMB_TAG = "$SMB_CACHE" +) + +type SMBAccessorArgs struct { + Hosts *ordereddict.Dict `vfilter:"required,field=hosts,doc=A dict mapping hostname to connection strings. The connection string consists of username and password joined by colon (e.g. fred:hunter2 )."` +} + +// Real implementation for non windows OSs: +type SMBFileSystemAccessor struct { + root *accessors.OSPath + + scope vfilter.Scope +} + +func (self *SMBFileSystemAccessor) ParsePath(path string) (*accessors.OSPath, error) { + return self.root.Parse(path) +} + +func (self SMBFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "smb", + Description: `Allows access to SMB shares.`, + Permissions: []acls.ACL_PERMISSION{acls.NETWORK}, + ScopeVar: constants.SMB_CREDENTIALS, + ArgType: &SMBAccessorArgs{}, + } +} + +func (self *SMBFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + return &SMBFileSystemAccessor{ + root: self.root, + scope: scope, + }, nil +} + +func (self *SMBFileSystemAccessor) Lstat(filename string) (accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +func (self *SMBFileSystemAccessor) LstatWithOSPath( + full_path *accessors.OSPath) (accessors.FileInfo, error) { + + fs, directory, closer, err := self.getMount(full_path) + if err != nil { + return nil, err + } + defer closer() + + lstat, err := fs.Lstat(directory) + if err != nil { + return nil, err + } + + return makeFileInfo(lstat, full_path), nil +} + +func (self *SMBFileSystemAccessor) getSession(full_path *accessors.OSPath) ( + *smb2.Session, func(), error) { + if len(full_path.Components) == 0 { + return nil, nil, errors.New("First path component for smb accessor must be a server name or IP") + } + + cache, pres := vql_subsystem.CacheGet(self.scope, SMB_TAG).(*SMBMountCache) + if !pres { + cache = NewSMBMountCache(self.scope) + vql_subsystem.CacheSet(self.scope, SMB_TAG, cache) + } + + server_name := full_path.Components[0] + connection, closer, err := cache.GetHandle(server_name) + if err != nil { + return nil, nil, err + } + + return connection.Session(), closer, nil +} + +func (self *SMBFileSystemAccessor) getMount(full_path *accessors.OSPath) ( + *smb2.Share, string, func(), error) { + if len(full_path.Components) < 2 { + return nil, "", nil, errors.New("SMBFileSystemAccessor.LstatWithOSPath requires at least a server name and share name.") + } + + session, closer, err := self.getSession(full_path) + if err != nil { + return nil, "", nil, err + } + + share := full_path.Components[1] + fs, err := session.Mount(share) + if err != nil { + return nil, "", nil, err + } + + directory := "." + if len(full_path.Components) > 2 { + directory = strings.Join(full_path.Components[2:], "\\") + } + + return fs, directory, closer, nil +} + +func (self *SMBFileSystemAccessor) ReadDir(dir string) ([]accessors.FileInfo, error) { + full_path, err := self.root.Parse(dir) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(full_path) +} + +func (self *SMBFileSystemAccessor) listShares( + full_path *accessors.OSPath) ([]accessors.FileInfo, error) { + + session, closer, err := self.getSession(full_path) + if err != nil { + return nil, err + } + defer closer() + + names, err := session.ListSharenames() + if err != nil { + return nil, err + } + + var result []accessors.FileInfo + for _, name := range names { + finfo := &accessors.VirtualFileInfo{ + Path: full_path.Append(name), + IsDir_: true, + } + result = append(result, finfo) + } + return result, nil +} + +func (self *SMBFileSystemAccessor) ReadDirWithOSPath( + full_path *accessors.OSPath) ([]accessors.FileInfo, error) { + if len(full_path.Components) == 0 { + return nil, errors.New("First path component for smb accessor must be a server name or IP") + } + + // If we only have a server name, it means to list the shares + if len(full_path.Components) == 1 { + return self.listShares(full_path) + } + + cache, pres := vql_subsystem.CacheGet(self.scope, SMB_TAG).(*SMBMountCache) + if !pres { + cache = NewSMBMountCache(self.scope) + vql_subsystem.CacheSet(self.scope, SMB_TAG, cache) + } + + server_name := full_path.Components[0] + connection, closer, err := cache.GetHandle(server_name) + if err != nil { + return nil, err + } + defer closer() + + share := full_path.Components[1] + fs, err := connection.Mount(share) + if err != nil { + return nil, err + } + + directory := "." + if len(full_path.Components) > 2 { + directory = strings.Join(full_path.Components[2:], "\\") + } + + matches, err := fs.ReadDir(directory) + if err != nil { + return nil, err + } + + var result []accessors.FileInfo + for _, match := range matches { + result = append(result, makeFileInfo( + match, full_path.Append(match.Name()))) + } + return result, nil +} + +func makeFileInfo(finfo fs.FileInfo, + full_path *accessors.OSPath) *accessors.VirtualFileInfo { + result := &accessors.VirtualFileInfo{ + IsDir_: finfo.IsDir(), + Size_: finfo.Size(), + Path: full_path, + Mtime_: finfo.ModTime(), + } + + sys, ok := finfo.Sys().(*smb2.FileStat) + if ok { + result.Atime_ = sys.LastAccessTime + result.Ctime_ = sys.ChangeTime + result.Btime_ = sys.CreationTime + } + return result +} + +// Wrap the os.File object to keep track of open file handles. +type SMBFileWrapper struct { + *smb2.File + closed bool +} + +func (self *SMBFileWrapper) DebugString() string { + return fmt.Sprintf("SMBFileWrapper %v (closed %v)", self.Name(), self.closed) +} + +func (self *SMBFileWrapper) Close() error { + smbAccessorCurrentOpened.Dec() + self.closed = true + return self.File.Close() +} + +func (self *SMBFileSystemAccessor) Open(path string) (accessors.ReadSeekCloser, error) { + // Clean the path + full_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self *SMBFileSystemAccessor) OpenWithOSPath( + full_path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + + fs, path, closer, err := self.getMount(full_path) + if err != nil { + return nil, err + } + defer closer() + + file_obj, err := fs.Open(path) + if err != nil { + return nil, err + } + + smbAccessorCurrentOpened.Inc() + return &SMBFileWrapper{File: file_obj}, nil +} + +func init() { + root_path := &accessors.OSPath{ + Manipulator: &SMBPathManipulator{}, + } + accessors.Register(&SMBFileSystemAccessor{ + root: root_path, + }) +} diff --git a/accessors/sparse/ranged.go b/accessors/sparse/ranged.go new file mode 100644 index 000000000..d0a11e00c --- /dev/null +++ b/accessors/sparse/ranged.go @@ -0,0 +1,157 @@ +package sparse + +import ( + "bufio" + "bytes" + "fmt" + "io" + "sync" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/zip" + actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" + vfilter "www.velocidex.com/golang/vfilter" +) + +type RangedReaderPath struct { + JsonlRanges string `json:"jsonl"` + Index string `json:"index"` +} + +func parseIndexRanges(serialized []byte) (*actions_proto.Index, error) { + arg := &RangedReaderPath{} + err := json.Unmarshal(serialized, arg) + if err != nil { + return nil, err + } + + index := &actions_proto.Index{} + + if arg.JsonlRanges != "" { + reader := bufio.NewReader(bytes.NewReader([]byte(arg.JsonlRanges))) + for { + row_data, err := reader.ReadBytes('\n') + if err != nil || len(row_data) == 0 { + return index, nil + } + + item := &actions_proto.Range{} + err = json.Unmarshal(row_data, item) + if err == nil { + index.Ranges = append(index.Ranges, item) + } + } + } + + if arg.Index != "" { + result := &actions_proto.Index{} + err = json.Unmarshal([]byte(arg.Index), result) + if err == nil { + index.Ranges = append(index.Ranges, result.Ranges...) + } + } + + return index, nil +} + +type RangedReader struct { + mu sync.Mutex + size int64 + offset int64 + + // A file handle to the underlying file. + handle accessors.ReadSeekCloser + reader_at io.ReaderAt +} + +func (self *RangedReader) Read(buf []byte) (int, error) { + self.mu.Lock() + defer self.mu.Unlock() + + n, err := self.reader_at.ReadAt(buf, self.offset) + self.offset += int64(n) + + // Range is past the end of file + return n, err +} + +func (self *RangedReader) Seek(offset int64, whence int) (int64, error) { + self.mu.Lock() + defer self.mu.Unlock() + + switch whence { + case 0: + self.offset = offset + case 1: + self.offset += offset + case 2: + self.offset = self.size + } + + return int64(self.offset), nil +} + +func (self *RangedReader) Close() error { + return self.handle.Close() +} + +func (self *RangedReader) LStat() (accessors.FileInfo, error) { + return &SparseFileInfo{size: self.size}, nil +} + +func GetRangedReaderFile(full_path *accessors.OSPath, scope vfilter.Scope) ( + zip.ReaderStat, error) { + if len(full_path.Components) == 0 { + return nil, fmt.Errorf("Ranged accessor expects a JSON sparse definition.") + } + + // The Path is a serialized ranges map. + index, err := parseIndexRanges([]byte(full_path.Components[0])) + if err != nil { + scope.Log("Ranged accessor expects ranges as path, for example: '[{Offset:0, Length: 10},{Offset:10,length:20}]'") + return nil, err + } + + pathspec := full_path.PathSpec() + + accessor, err := accessors.GetAccessor(pathspec.DelegateAccessor, scope) + if err != nil { + scope.Log("%v: did you provide a PathSpec?", err) + return nil, err + } + + fd, err := accessor.Open(pathspec.GetDelegatePath()) + if err != nil { + scope.Log("sparse: Failed to open delegate %v: %v", + pathspec.GetDelegatePath(), err) + return nil, err + } + + // Devices can not be stat'ed + size := int64(0) + if len(index.Ranges) > 0 { + last := index.Ranges[len(index.Ranges)-1] + size = last.FileOffset + last.FileLength + } + + return &RangedReader{ + handle: fd, + size: size, + reader_at: &utils.RangedReader{ + ReaderAt: utils.MakeReaderAtter(fd), + Index: index, + }, + }, nil +} + +func init() { + accessors.Register(accessors.DescribeAccessor( + zip.NewGzipFileSystemAccessor( + accessors.MustNewPathspecOSPath(""), GetRangedReaderFile), + accessors.AccessorDescriptor{ + Name: "ranged", + Description: `Reconstruct sparse files from idx and base`, + })) +} diff --git a/accessors/sparse/sparse.go b/accessors/sparse/sparse.go new file mode 100644 index 000000000..484b93d09 --- /dev/null +++ b/accessors/sparse/sparse.go @@ -0,0 +1,256 @@ +package sparse + +import ( + "fmt" + "io" + "os" + "sync" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/zip" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/uploads" + vfilter "www.velocidex.com/golang/vfilter" +) + +const PAGE_SIZE = 0x1000 + +func parseRanges(serialized []byte) ([]*uploads.Range, error) { + ranges := []*uploads.Range{} + err := json.Unmarshal(serialized, &ranges) + if err != nil { + return nil, err + } + + result := make([]*uploads.Range, 0, len(ranges)) + offset := int64(0) + for _, r := range ranges { + if r.Offset != offset { + result = append(result, &uploads.Range{ + Offset: offset, + Length: r.Offset - offset, + IsSparse: true, + }) + } + result = append(result, r) + offset = r.Offset + r.Length + } + + return result, nil +} + +type SparseReader struct { + mu sync.Mutex + offset int64 + size int64 + + // A file handle to the underlying file. + handle accessors.ReadSeekCloser + ranges []*uploads.Range +} + +// Repeat the read operation one page at the time in order to retrieve +// as much data as possible. +func (self *SparseReader) readDistinctPages(buf []byte) (int, error) { + page_count := len(buf) / PAGE_SIZE + if page_count <= 1 { + return page_count * PAGE_SIZE, nil + } + + // Read as many pages as possible into the buffer ignoring errors. + for i := 0; i < page_count; i += 1 { + buf_start := i * PAGE_SIZE + buf_end := buf_start + PAGE_SIZE + + // Repeat the read with a single page at the time. + _, err := self.handle.Seek(self.offset, os.SEEK_SET) + if err != nil { + return 0, err + } + + _, err = self.handle.Read(buf[buf_start:buf_end]) + if err != nil { + // Error occured reading a single page, zero + // it out and skip the page. + for i := buf_start; i < buf_end; i++ { + buf[i] = 0 + } + self.offset += PAGE_SIZE + } + } + + return page_count * PAGE_SIZE, nil +} + +func (self *SparseReader) Read(buf []byte) (int, error) { + self.mu.Lock() + defer self.mu.Unlock() + + current_range, next_range := uploads.GetNextRange(self.offset, self.ranges) + // Current offset is inside the range. + if current_range != nil { + to_read := current_range.Offset + current_range.Length - self.offset + if to_read > int64(len(buf)) { + to_read = int64(len(buf)) + } + + if current_range.IsSparse { + for i := int64(0); i < to_read; i++ { + buf[i] = 0 + } + } else { + // Read memory from process at specified offset. + _, err := self.handle.Seek(self.offset, os.SEEK_SET) + if err != nil { + return 0, err + } + + _, err = self.handle.Read(buf[:to_read]) + + // A read error occured - split the read into multiple page + // size reads to get as much data as we can out of the + // region. Note: We always return as much data as was + // required, we simply null pad the missing data. Therefore if + // a reader askes to read from a memory region that contains + // no data, we never return an error - just zero pad those + // regions. + if err != nil { + return self.readDistinctPages(buf) + } + } + + // Advance the read pointer. + self.offset += to_read + + return int(to_read), nil + } + + // The current offset is not inside any range so we null pad until + // the next range. + if next_range != nil { + to_read := next_range.Offset - self.offset + if to_read > int64(len(buf)) { + to_read = int64(len(buf)) + } + + // Clear the buffer + for i := range buf[:to_read] { + buf[i] = 0 + } + self.offset += to_read + return int(to_read), nil + } + + // Range is past the end of file + return 0, io.EOF +} + +func (self *SparseReader) Ranges() []uploads.Range { + self.mu.Lock() + defer self.mu.Unlock() + + result := []uploads.Range{} + size := int64(0) + for _, rng := range self.ranges { + // Fill in a sparse range if needed + if rng.Offset > size { + result = append(result, uploads.Range{ + Offset: size, + Length: rng.Offset - size, + IsSparse: true, + }) + } + + // Move the pointer past the end of this range. + size = rng.Offset + rng.Length + + // Add a real data run + result = append(result, *rng) + } + return result +} + +func (self *SparseReader) Seek(offset int64, whence int) (int64, error) { + self.mu.Lock() + defer self.mu.Unlock() + + switch whence { + case 0: + self.offset = offset + case 1: + self.offset += offset + case 2: + self.offset = self.size + } + + return int64(self.offset), nil +} + +func (self *SparseReader) Close() error { + return self.handle.Close() +} + +func (self *SparseReader) LStat() (accessors.FileInfo, error) { + return &SparseFileInfo{size: self.size}, nil +} + +type SparseFileInfo struct { + accessors.VirtualFileInfo + size int64 +} + +func (self SparseFileInfo) Size() int64 { + return self.size +} + +func GetSparseFile(full_path *accessors.OSPath, scope vfilter.Scope) ( + zip.ReaderStat, error) { + if len(full_path.Components) == 0 { + return nil, fmt.Errorf("Sparse accessor expects a JSON sparse definition.") + } + + // The Path is a serialized ranges map. + ranges, err := parseRanges([]byte(full_path.Components[0])) + if err != nil { + scope.Log("Sparse accessor expects ranges as path, for example: '[{Offset:0, Length: 10},{Offset:10,length:20}]'") + return nil, err + } + + pathspec := full_path.PathSpec() + + accessor, err := accessors.GetAccessor(pathspec.DelegateAccessor, scope) + if err != nil { + scope.Log("%v: did you provide a URL or PathSpec?", err) + return nil, err + } + + fd, err := accessor.Open(pathspec.GetDelegatePath()) + if err != nil { + scope.Log("sparse: Failed to open delegate %v: %v", + pathspec.GetDelegatePath(), err) + return nil, err + } + + // Devices can not be stat'ed + size := int64(0) + if len(ranges) > 0 { + last := ranges[len(ranges)-1] + size = last.Offset + last.Length + } + + return &SparseReader{ + handle: fd, + size: size, + ranges: ranges, + }, nil +} + +func init() { + accessors.Register(accessors.DescribeAccessor( + zip.NewGzipFileSystemAccessor( + accessors.MustNewPathspecOSPath(""), GetSparseFile), + accessors.AccessorDescriptor{ + Name: "sparse", + Description: `Allows reading another file by overlaying a sparse map on top of it.`, + })) +} diff --git a/accessors/sparse/sparse_test.go b/accessors/sparse/sparse_test.go new file mode 100644 index 000000000..c802bfef7 --- /dev/null +++ b/accessors/sparse/sparse_test.go @@ -0,0 +1,33 @@ +package sparse + +import ( + "io/ioutil" + "testing" + + "www.velocidex.com/golang/velociraptor/accessors" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + + _ "www.velocidex.com/golang/velociraptor/accessors/data" +) + +func TestAccessorSparse(t *testing.T) { + scope := vql_subsystem.MakeScope() + accessor, err := accessors.GetAccessor("sparse", scope) + assert.NoError(t, err) + + // The Path is really a json encoded sparse map. + pathspec := &accessors.PathSpec{ + DelegateAccessor: "data", + DelegatePath: "This is a bit of text", + Path: `[{"length":5,"offset":0},{"length":3,"offset":10}]`, + } + + fd, err := accessor.Open(pathspec.String()) + assert.NoError(t, err) + + data, err := ioutil.ReadAll(fd) + assert.NoError(t, err) + + assert.Equal(t, "This \x00\x00\x00\x00\x00bit", string(data)) +} diff --git a/accessors/ssh/file_info.go b/accessors/ssh/file_info.go new file mode 100644 index 000000000..ec69c567b --- /dev/null +++ b/accessors/ssh/file_info.go @@ -0,0 +1,96 @@ +package ssh + +import ( + "errors" + "os" + "time" + + "github.com/Velocidex/ordereddict" + "github.com/pkg/sftp" + "www.velocidex.com/golang/velociraptor/accessors" +) + +type SFTPFileInfo struct { + _FileInfo os.FileInfo + _full_path *accessors.OSPath +} + +func NewSFTPFileInfo(base os.FileInfo, path *accessors.OSPath) *SFTPFileInfo { + return &SFTPFileInfo{ + _FileInfo: base, + _full_path: path, + } +} + +func (self *SFTPFileInfo) OSPath() *accessors.OSPath { + return self._full_path +} + +func (self *SFTPFileInfo) Size() int64 { + return self._FileInfo.Size() +} + +func (self *SFTPFileInfo) Name() string { + return self._FileInfo.Name() +} + +func (self *SFTPFileInfo) IsDir() bool { + return self._FileInfo.IsDir() +} + +func (self *SFTPFileInfo) ModTime() time.Time { + return self._FileInfo.ModTime() +} + +func (self *SFTPFileInfo) Mode() os.FileMode { + return self._FileInfo.Mode() +} + +func (self *SFTPFileInfo) Sys() interface{} { + return self._FileInfo.Sys() +} + +func (self *SFTPFileInfo) Dev() uint64 { + return 0 +} + +func (self *SFTPFileInfo) Data() *ordereddict.Dict { + result := ordereddict.NewDict() + return result +} + +func (self *SFTPFileInfo) FullPath() string { + return self._full_path.String() +} + +func (self *SFTPFileInfo) IsLink() bool { + return self.Mode()&os.ModeSymlink != 0 +} + +func (self *SFTPFileInfo) GetLink() (*accessors.OSPath, error) { + return nil, errors.New("Symlink not supported") +} + +func (self *SFTPFileInfo) _Sys() *sftp.FileStat { + return self._FileInfo.Sys().(*sftp.FileStat) +} + +func (self *SFTPFileInfo) Btime() time.Time { + return time.Time{} +} + +func (self *SFTPFileInfo) Mtime() time.Time { + return time.Unix(int64(self._Sys().Mtime), 0) +} + +func (self *SFTPFileInfo) Ctime() time.Time { + return time.Time{} +} + +func (self *SFTPFileInfo) Atime() time.Time { + return time.Unix(int64(self._Sys().Atime), 0) +} + +type SFTPFileWrapper struct { + *sftp.File +} diff --git a/accessors/ssh/session.go b/accessors/ssh/session.go new file mode 100644 index 000000000..de735c783 --- /dev/null +++ b/accessors/ssh/session.go @@ -0,0 +1,144 @@ +package ssh + +import ( + "context" + "errors" + "fmt" + + "golang.org/x/crypto/ssh" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/arg_parser" + "www.velocidex.com/golang/vfilter/utils/dict" +) + +type SSHAccessorArgs struct { + Secret string `vfilter:"optional,field=secret,doc=The name of a secret to use."` + Username string `vfilter:"optional,field=username,doc=The username to use to log into the remote system."` + Password string `vfilter:"optional,field=password,doc=The password to use to log into the remote system."` + PrivateKey string `vfilter:"optional,field=private_key,doc=A private key to use to log into the remote system instead of a password."` + Hostname string `vfilter:"optional,field=hostname,doc=The hostname to log into."` +} + +func GetSSHClient(scope vfilter.Scope) ( + client *ssh.Client, closer func() error, err error) { + + // TODO: Extract the context from the scope. + ctx := context.TODO() + + setting, pres := scope.Resolve(constants.SSH_CONFIG) + if !pres { + return nil, nil, errors.New("Configure the 'ssh' accessor using 'LET SSH_CONFIG <= dict(...)'") + } + + args := dict.RowToDict(ctx, scope, setting) + arg := &SSHAccessorArgs{} + err = arg_parser.ExtractArgsWithContext(ctx, scope, args, arg) + if err != nil { + return nil, nil, err + } + + err = maybeForceSecrets(ctx, scope, arg) + if err != nil { + return nil, nil, err + } + + if arg.Secret != "" { + arg, err = getSecret(ctx, scope, arg.Secret) + if err != nil { + return nil, nil, err + } + } + + config := &ssh.ClientConfig{ + User: arg.Username, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + if arg.Password != "" { + config.Auth = append(config.Auth, ssh.Password(arg.Password)) + } + + if arg.PrivateKey != "" { + // Attempt to parse it + signer, err := ssh.ParsePrivateKey([]byte(arg.PrivateKey)) + if err != nil { + return nil, nil, fmt.Errorf("ssh: While parsing private key: %w", err) + } + + config.Auth = append(config.Auth, ssh.PublicKeys(signer)) + } + + if arg.Hostname == "" { + return nil, nil, errors.New("ssh: No hostname specified in SSH_CONFIG") + } + + client, err = ssh.Dial("tcp", arg.Hostname, config) + if err != nil { + return nil, nil, err + } + + scope.Log("INFO:ssh: Initiated connection to host %v", arg.Hostname) + + return client, client.Close, nil +} + +func maybeForceSecrets( + ctx context.Context, scope vfilter.Scope, arg *SSHAccessorArgs) error { + + // Not running on the server, secrets dont work. + config_obj, ok := vql_subsystem.GetServerConfig(scope) + if !ok { + return nil + } + + if config_obj.Security == nil { + return nil + } + + if !config_obj.Security.VqlMustUseSecrets { + return nil + } + + // If an explicit secret is defined let it filter the URLs. + if arg.Secret != "" { + return nil + } + + return utils.SecretsEnforced +} + +func getSecret( + ctx context.Context, + scope vfilter.Scope, + secret string) (*SSHAccessorArgs, error) { + config_obj, ok := vql_subsystem.GetServerConfig(scope) + if !ok { + return nil, errors.New("Secrets may only be used on the server") + } + + secrets_service, err := services.GetSecretsService(config_obj) + if err != nil { + return nil, err + } + + principal := vql_subsystem.GetPrincipal(scope) + + secret_record, err := secrets_service.GetSecret(ctx, principal, + constants.SSH_PRIVATE_KEY, secret) + if err != nil { + return nil, err + } + + // Override the following from the secret + arg := &SSHAccessorArgs{ + Username: secret_record.GetString("username"), + Hostname: secret_record.GetString("hostname"), + Password: secret_record.GetString("password"), + PrivateKey: secret_record.GetString("private_key"), + } + return arg, nil +} diff --git a/accessors/ssh/ssh.go b/accessors/ssh/ssh.go new file mode 100644 index 000000000..d27e306aa --- /dev/null +++ b/accessors/ssh/ssh.go @@ -0,0 +1,143 @@ +package ssh + +import ( + "errors" + "strings" + + "github.com/pkg/sftp" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/constants" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +var ( + ErrNotFound = errors.New("file not found") + ErrNotAvailable = errors.New("File content not available") +) + +type SSHFileSystemAccessor struct { + scope vfilter.Scope + sftp_client *sftp.Client +} + +func (self SSHFileSystemAccessor) New(scope vfilter.Scope) (accessors.FileSystemAccessor, error) { + ssh_client, closer, err := GetSSHClient(scope) + if err != nil { + return nil, err + } + + sftp_client, err := sftp.NewClient(ssh_client) + if err != nil { + ssh_client.Close() + return nil, err + } + + // Close the ssh client when the scope destroys. + err = vql_subsystem.GetRootScope(scope).AddDestructor(func() { + sftp_client.Close() + _ = closer() + }) + if err != nil { + sftp_client.Close() + _ = closer() + return nil, err + } + + return &SSHFileSystemAccessor{ + scope: scope, + sftp_client: sftp_client, + }, nil +} + +func (self SSHFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "ssh", + Description: `Access a remote system's filesystem via SSH/SFTP.`, + Permissions: []acls.ACL_PERMISSION{acls.NETWORK}, + ScopeVar: constants.SSH_CONFIG, + ArgType: &SSHAccessorArgs{}, + } +} + +func (self SSHFileSystemAccessor) Lstat(filename string) ( + accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +func (self SSHFileSystemAccessor) LstatWithOSPath(filename *accessors.OSPath) ( + accessors.FileInfo, error) { + + path := "/" + strings.Join(filename.Components, "/") + info, err := self.sftp_client.Lstat(path) + if err != nil { + return nil, err + } + + return NewSFTPFileInfo(info, filename), nil +} + +func (self SSHFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewGenericOSPath(path) +} + +func (self SSHFileSystemAccessor) ReadDir(filename string) ( + []accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(full_path) +} + +func (self SSHFileSystemAccessor) ReadDirWithOSPath( + filename *accessors.OSPath) ( + result []accessors.FileInfo, err error) { + + path := "/" + strings.Join(filename.Components, "/") + dir, err := self.sftp_client.ReadDir(path) + if err != nil { + return nil, err + } + + for _, d := range dir { + child := filename.Append(d.Name()) + result = append(result, NewSFTPFileInfo(d, child)) + } + + return result, err +} + +func (self SSHFileSystemAccessor) Open(filename string) ( + accessors.ReadSeekCloser, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self SSHFileSystemAccessor) OpenWithOSPath(filename *accessors.OSPath) ( + accessors.ReadSeekCloser, error) { + + path := "/" + strings.Join(filename.Components, "/") + fd, err := self.sftp_client.Open(path) + if err != nil { + return nil, err + } + + return &SFTPFileWrapper{fd}, nil +} + +func init() { + accessors.Register(&SSHFileSystemAccessor{}) +} diff --git a/accessors/utils.go b/accessors/utils.go new file mode 100644 index 000000000..25c6970a0 --- /dev/null +++ b/accessors/utils.go @@ -0,0 +1,27 @@ +package accessors + +import "fmt" + +func ParsePath(path, path_type string) (res *OSPath, err error) { + switch path_type { + case "linux": + res, err = NewLinuxOSPath(path) + case "windows": + res, err = NewWindowsOSPath(path) + case "registry": + res, err = NewWindowsRegistryPath(path) + case "ntfs": + res, err = NewWindowsNTFSPath(path) + case "", "generic": + res, err = NewGenericOSPath(path) + case "pathspec": + res, err = NewPathspecOSPath(path) + + case "zip": + res, err = NewZipFilePath(path) + + default: + err = fmt.Errorf("Unknown path type: %v (should be one of windows,linux,generic)", path_type) + } + return res, err +} diff --git a/accessors/vfs/fixtures/TestVFSAccessor.golden b/accessors/vfs/fixtures/TestVFSAccessor.golden new file mode 100644 index 000000000..6f1198465 --- /dev/null +++ b/accessors/vfs/fixtures/TestVFSAccessor.golden @@ -0,0 +1,14 @@ +{ + "DirectoryListings": [ + "/vfs_test/C:", + "/vfs_test/C:/Windows", + "/vfs_test/C:/Windows/File1.txt", + "/vfs_test/C:/Windows/System32", + "/vfs_test/C:/Windows/System32/File.txt", + "/vfs_test/D:" + ], + "FileContents": { + "/vfs_test/C:/Windows/File1.txt": "File in Windows", + "/vfs_test/C:/Windows/System32/File.txt": "File in System32" + } +} \ No newline at end of file diff --git a/accessors/vfs/vfs.go b/accessors/vfs/vfs.go new file mode 100644 index 000000000..e4291a179 --- /dev/null +++ b/accessors/vfs/vfs.go @@ -0,0 +1,275 @@ +package vfs + +import ( + "context" + "errors" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + flows_proto "www.velocidex.com/golang/velociraptor/flows/proto" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +var ( + ErrNotFound = errors.New("file not found") + ErrNotAvailable = errors.New("File content not available") + ErrInvalidRow = errors.New("Stored row is invalid") +) + +type VFSFileSystemAccessor struct { + ctx context.Context + client_id string + config_obj *config_proto.Config + file_store_accessor accessors.FileSystemAccessor +} + +func (self VFSFileSystemAccessor) New( + scope vfilter.Scope) (accessors.FileSystemAccessor, error) { + + config_obj, ok := vql_subsystem.GetServerConfig(scope) + if !ok { + return nil, errors.New("vfs accessor: can only run on the server") + } + + client_id, pres := scope.Resolve("ClientId") + if !pres { + return nil, errors.New("vfs accessor: ClientId does not exist in the scope") + } + + client_id_str, ok := client_id.(string) + if !ok { + return nil, errors.New("vfs accessor: ClientId does not exist in the scope") + } + + accessor, err := accessors.GetAccessor("fs", scope) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.Background()) + err = vql_subsystem.GetRootScope(scope).AddDestructor(cancel) + if err != nil { + return nil, err + } + + return &VFSFileSystemAccessor{ + ctx: ctx, + client_id: client_id_str, + file_store_accessor: accessor, + config_obj: config_obj, + }, nil +} + +func (self VFSFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "vfs", + Description: `Access client's VFS filesystem on the server.`, + Permissions: []acls.ACL_PERMISSION{acls.READ_RESULTS}, + } +} + +func (self VFSFileSystemAccessor) Lstat(filename string) ( + accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +func (self VFSFileSystemAccessor) LstatWithOSPath(filename *accessors.OSPath) ( + accessors.FileInfo, error) { + vfs_service, err := services.GetVFSService(self.config_obj) + if err != nil { + return nil, err + } + + res, err := vfs_service.ListDirectoryFiles(self.ctx, + self.config_obj, &api_proto.GetTableRequest{ + Rows: 1000, + ClientId: self.client_id, + VfsComponents: filename.Dirname().Components, + }) + if err != nil { + return nil, err + } + + // Find the row that matches this filename + for _, r := range res.Rows { + var row []interface{} + _ = json.Unmarshal([]byte(r.Json), &row) + if len(row) < 12 { + continue + } + + name, ok := row[5].(string) + if ok && name == filename.Basename() { + return rowCellToFSInfo(row) + } + } + + return nil, ErrNotFound +} + +func (self VFSFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewGenericOSPath(path) +} + +func (self VFSFileSystemAccessor) ReadDir(filename string) ( + []accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(full_path) +} + +func (self VFSFileSystemAccessor) ReadDirWithOSPath( + filename *accessors.OSPath) ( + []accessors.FileInfo, error) { + + vfs_service, err := services.GetVFSService(self.config_obj) + if err != nil { + return nil, err + } + + res, err := vfs_service.ListDirectoryFiles(self.ctx, + self.config_obj, &api_proto.GetTableRequest{ + Rows: 1000, + ClientId: self.client_id, + VfsComponents: filename.Components, + }) + if err != nil { + return nil, err + } + + result := []accessors.FileInfo{} + for _, r := range res.Rows { + var row []interface{} + _ = json.Unmarshal([]byte(r.Json), &row) + if len(row) < 12 { + continue + } + + fs_info, err := rowCellToFSInfo(row) + if err != nil { + continue + } + result = append(result, fs_info) + } + + return result, nil +} + +func (self VFSFileSystemAccessor) Open(filename string) ( + accessors.ReadSeekCloser, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self VFSFileSystemAccessor) OpenWithOSPath(filename *accessors.OSPath) ( + accessors.ReadSeekCloser, error) { + + vfs_service, err := services.GetVFSService(self.config_obj) + if err != nil { + return nil, err + } + + res, err := vfs_service.ListDirectoryFiles(self.ctx, + self.config_obj, &api_proto.GetTableRequest{ + Rows: 1000, + ClientId: self.client_id, + VfsComponents: filename.Dirname().Components, + }) + if err != nil { + return nil, err + } + + // Find the row that matches this filename + for _, r := range res.Rows { + var row []interface{} + _ = json.Unmarshal([]byte(r.Json), &row) + if len(row) < 12 { + continue + } + + name, ok := row[5].(string) + if !ok { + continue + } + + if name == filename.Basename() { + // Check if it has a download link + record := &flows_proto.VFSDownloadInfo{} + err = utils.ParseIntoProtobuf(row[0], record) + if err != nil || record.Name == "" { + return nil, ErrNotAvailable + } + + return self.file_store_accessor.OpenWithOSPath( + accessors.MustNewFileStorePath("").Append(record.Components...)) + } + } + + return nil, ErrNotFound +} + +func rowCellToFSInfo(cell []interface{}) (accessors.FileInfo, error) { + components := utils.ConvertToStringSlice(cell[2]) + if len(components) == 0 { + return nil, ErrInvalidRow + } + + size, ok := utils.ToInt64(cell[6]) + if !ok { + return nil, ErrInvalidRow + } + + mode, ok := cell[7].(string) + if !ok { + return nil, ErrInvalidRow + } + + is_dir := len(mode) > 1 && mode[0] == 'd' + + // The Accessor + components is the path of the item + ospath, ok := cell[3].(string) + if !ok { + return nil, ErrInvalidRow + } + + path := accessors.MustNewGenericOSPath(ospath).Append(components...) + fs_info := &accessors.VirtualFileInfo{ + Path: path, + IsDir_: is_dir, + Size_: size, + Data_: ordereddict.NewDict(), + } + + // The download pointer allows us to fetch the file itself. + record := &flows_proto.VFSDownloadInfo{} + err := utils.ParseIntoProtobuf(cell[0], record) + if err == nil { + fs_info.Data_.Set("DownloadInfo", record) + } + + return fs_info, nil +} + +func init() { + accessors.Register(&VFSFileSystemAccessor{}) +} diff --git a/accessors/vfs/vfs_test.go b/accessors/vfs/vfs_test.go new file mode 100644 index 000000000..2a66371fb --- /dev/null +++ b/accessors/vfs/vfs_test.go @@ -0,0 +1,256 @@ +package vfs + +import ( + "sort" + "testing" + "time" + + "github.com/Velocidex/ordereddict" + "github.com/stretchr/testify/suite" + "www.velocidex.com/golang/velociraptor/accessors" + actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/artifacts/assets" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/file_store/test_utils" + flows_proto "www.velocidex.com/golang/velociraptor/flows/proto" + "www.velocidex.com/golang/velociraptor/glob" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vtesting" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" + + "www.velocidex.com/golang/velociraptor/accessors/file_store" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + _ "www.velocidex.com/golang/velociraptor/vql/filesystem" + _ "www.velocidex.com/golang/velociraptor/vql/golang" + _ "www.velocidex.com/golang/velociraptor/vql/parsers" +) + +type testCases struct { + Path string + IsDir bool + Content string +} + +var ( + Filesystem = []testCases{ + {Path: "C:", IsDir: true}, + {Path: "C:\\Windows", IsDir: true}, + {Path: "C:\\Windows\\System32", IsDir: true}, + {Path: "C:\\Windows\\File1.txt", Content: "File in Windows"}, + {Path: "C:\\Windows\\System32\\File.txt", Content: "File in System32"}, + {Path: "D:", IsDir: true}, + } + + artifacts_used = []string{ + "/artifacts/definitions/System/VFS/ListDirectory.yaml", + "/artifacts/definitions/System/VFS/DownloadFile.yaml", + } +) + +func setVirtualFilesystem() { + root_path := accessors.MustNewWindowsOSPath("") + root_fs_accessor := accessors.NewVirtualFilesystemAccessor(root_path) + for _, c := range Filesystem { + root_fs_accessor.SetVirtualFileInfo(&accessors.VirtualFileInfo{ + Path: accessors.MustNewWindowsOSPath(c.Path), + IsDir_: c.IsDir, + RawData: []byte(c.Content), + Size_: int64(len(c.Content)), + }) + } + + // Install this under a new accessor name. + accessors.Register(accessors.DescribeAccessor( + root_fs_accessor, accessors.AccessorDescriptor{ + Name: "vfs_test", + })) +} + +type TestSuite struct { + test_utils.TestSuite +} + +func (self *TestSuite) SetupTest() { + self.ConfigObj = self.LoadConfig() + self.ConfigObj.Services.HuntDispatcher = true + self.ConfigObj.Services.HuntManager = true + self.ConfigObj.Services.ServerArtifacts = true + self.ConfigObj.Services.VfsService = true + + self.TestSuite.SetupTest() +} + +func (self *TestSuite) TestVFSAccessor() { + defer utils.MockTime(utils.NewMockClock(time.Unix(10, 10)))() + defer utils.SetFlowIdForTests("F.1234")() + + setVirtualFilesystem() + + manager, _ := services.GetRepositoryManager(self.ConfigObj) + repository, err := manager.GetGlobalRepository(self.ConfigObj) + assert.NoError(self.T(), err) + + options := services.ArtifactOptions{ + ValidateArtifact: true, + ArtifactIsBuiltIn: true, + ArtifactIsCompiledIn: false, + } + + for _, a := range artifacts_used { + data, err := assets.ReadFile(a) + assert.NoError(self.T(), err) + + _, err = repository.LoadYaml(string(data), options) + assert.NoError(self.T(), err) + } + + launcher, err := services.GetLauncher(self.ConfigObj) + assert.NoError(self.T(), err) + + var acl_manager vql_subsystem.ACLManager = acl_managers.NullACLManager{} + + flow_id, err := launcher.ScheduleArtifactCollection(self.Ctx, self.ConfigObj, + acl_manager, repository, &flows_proto.ArtifactCollectorArgs{ + Artifacts: []string{"System.VFS.ListDirectory", "System.VFS.DownloadFile"}, + Creator: utils.GetSuperuserName(self.ConfigObj), + Specs: []*flows_proto.ArtifactSpec{ + { + Artifact: "System.VFS.DownloadFile", + Parameters: &flows_proto.ArtifactParameters{ + Env: []*actions_proto.VQLEnv{ + { + Key: "Accessor", + Value: "vfs_test", + }, + { + Key: "Components", + Value: "[]", + }, + { + Key: "Recursively", + Value: "Y", + }, + }, + }, + }, + { + Artifact: "System.VFS.ListDirectory", + Parameters: &flows_proto.ArtifactParameters{ + Env: []*actions_proto.VQLEnv{ + { + Key: "Accessor", + Value: "vfs_test", + }, + { + Key: "Components", + Value: "[]", + }, + { + Key: "Depth", + Value: "10", + }, + }, + }}, + }, + ClientId: constants.VELOCIRAPTOR_SERVER_CLIENT_ID, + }, utils.SyncCompleter) + assert.NoError(self.T(), err) + + // Wait here until the collection is completed. + vtesting.WaitUntil(time.Second*5, self.T(), func() bool { + flow, err := launcher.GetFlowDetails( + self.Ctx, self.ConfigObj, services.GetFlowOptions{}, + constants.VELOCIRAPTOR_SERVER_CLIENT_ID, flow_id) + assert.NoError(self.T(), err) + + return flow.Context.State == flows_proto.ArtifactCollectorContext_FINISHED + }) + + vfs_service, err := services.GetVFSService(self.ConfigObj) + assert.NoError(self.T(), err) + + // test_utils.GetMemoryFileStore(self.T(), self.ConfigObj).Debug() + + // Wait until the vfs service processes it + vtesting.WaitUntil(time.Second*5, self.T(), func() bool { + dir, err := vfs_service.ListDirectoryFiles(self.Ctx, self.ConfigObj, + &api_proto.GetTableRequest{ + Rows: 10, + ClientId: constants.VELOCIRAPTOR_SERVER_CLIENT_ID, + VfsComponents: []string{"vfs_test", "C:", "Windows"}, + }) + if err != nil { + return false + } + return dir.TotalRows > 0 + }) + + fs_factory := file_store.NewFileStoreFileSystemAccessor(self.ConfigObj) + accessors.Register(accessors.DescribeAccessor(fs_factory, + accessors.AccessorDescriptor{ + Name: "fs", + })) + + // Now create a download of this collection. + builder := services.ScopeBuilder{ + Config: self.ConfigObj, + ACLManager: acl_managers.NullACLManager{}, + Logger: logging.NewPlainLogger(self.ConfigObj, &logging.FrontendComponent), + Env: ordereddict.NewDict(). + Set("ClientId", constants.VELOCIRAPTOR_SERVER_CLIENT_ID), + } + + scope := manager.BuildScope(builder) + + // Now test the vfs_accessor. + accessor, err := accessors.GetAccessor("vfs", scope) + assert.NoError(self.T(), err) + + globber := glob.NewGlobber() + defer globber.Close() + + glob_path, err := accessors.NewGenericOSPath("/**") + assert.NoError(self.T(), err) + + globber.Add(glob_path) + + hits := []string{} + file_content := ordereddict.NewDict() + + for hit := range globber.ExpandWithContext( + self.Ctx, scope, self.ConfigObj, + accessors.MustNewGenericOSPath("vfs_test"), accessor) { + full_path := hit.OSPath().Path() + + hits = append(hits, full_path) + + if !hit.IsDir() { + data := make([]byte, 1024) + fd, err := accessor.OpenWithOSPath(hit.OSPath()) + assert.NoError(self.T(), err) + + n, err := fd.Read(data) + assert.NoError(self.T(), err) + file_content.Set(full_path, string(data[:n])) + fd.Close() + } + + } + sort.Strings(hits) + + golden := ordereddict.NewDict(). + Set("DirectoryListings", hits). + Set("FileContents", file_content) + + goldie.Assert(self.T(), "TestVFSAccessor", json.MustMarshalIndent(golden)) +} + +func TestVFSAccessor(t *testing.T) { + suite.Run(t, &TestSuite{}) +} diff --git a/accessors/vhdx/cache.go b/accessors/vhdx/cache.go new file mode 100644 index 000000000..44ec89abb --- /dev/null +++ b/accessors/vhdx/cache.go @@ -0,0 +1,107 @@ +package vhdx + +import ( + "sync" + + "github.com/Velocidex/go-vhdx/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +const ( + VHDX_CACHE_TAG = "__VHDX_CACHE" +) + +// Don't bother expiring this until the end of the query. +type vhdxCache struct { + mu sync.Mutex + + cache map[string]*VHDXFile +} + +func (self *vhdxCache) Get(key string) (*VHDXFile, bool) { + self.mu.Lock() + defer self.mu.Unlock() + + r, pres := self.cache[key] + return r, pres +} + +func (self *vhdxCache) Set(key string, r *VHDXFile) { + self.mu.Lock() + defer self.mu.Unlock() + + self.cache[key] = r +} + +func (self *vhdxCache) Close() { + self.mu.Lock() + defer self.mu.Unlock() + + for _, r := range self.cache { + if r.closer != nil { + r.closer() + } + } +} + +func getCachedVHDXFile( + full_path *accessors.OSPath, + accessor accessors.FileSystemAccessor, + scope vfilter.Scope) (*VHDXFile, error) { + + cache, pres := vql_subsystem.CacheGet(scope, VHDX_CACHE_TAG).(*vhdxCache) + if !pres { + cache = &vhdxCache{ + cache: make(map[string]*VHDXFile), + } + // Cache will remain alive for the duration of the query. + err := vql_subsystem.GetRootScope(scope).AddDestructor(cache.Close) + if err != nil { + cache.Close() + return nil, err + } + vql_subsystem.CacheSet(scope, VHDX_CACHE_TAG, cache) + } + + now := utils.GetTime().Now() + key := full_path.String() + res, pres := cache.Get(key) + if pres { + // Give a copy of the cache object so it can be seeked + // independently. + return res._Copy(), nil + } + + delegate, err := full_path.Delegate(scope) + if err != nil { + return nil, err + } + + fd, err := accessor.OpenWithOSPath(delegate) + if err != nil { + return nil, err + } + + file_obj, err := parser.NewVHDXFile(utils.MakeReaderAtter(fd)) + if err != nil { + return nil, err + } + + vhdx_file := &VHDXFile{ + reader: file_obj, + size: file_obj.Metadata.VirtualDiskSize, + closer: func() { + scope.Log("vhdx: Closing VHDX file %v\n", key) + fd.Close() + }, + } + + cache.Set(key, vhdx_file) + scope.Log("vhdx: Opened VHDX file %v in %v\n", key, + utils.GetTime().Now().Sub(now).String()) + + return vhdx_file, nil +} diff --git a/accessors/vhdx/vhdx.go b/accessors/vhdx/vhdx.go new file mode 100644 index 000000000..071784a1e --- /dev/null +++ b/accessors/vhdx/vhdx.go @@ -0,0 +1,109 @@ +package vhdx + +import ( + "io" + "os" + "sync" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/zip" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +type VHDXFile struct { + reader io.ReaderAt + + mu sync.Mutex + offset int64 + size uint64 + + closer func() +} + +// Lifetime is managed by the cache +func (self *VHDXFile) Close() error { + return nil +} + +func (self *VHDXFile) Read(buff []byte) (int, error) { + self.mu.Lock() + defer self.mu.Unlock() + + n, err := self.reader.ReadAt(buff, self.offset) + if err != nil { + return 0, err + } + + if n == 0 { + return 0, io.EOF + } + + self.offset += int64(n) + return n, err +} + +func (self *VHDXFile) Seek(offset int64, whence int) (int64, error) { + self.mu.Lock() + defer self.mu.Unlock() + + if whence == os.SEEK_SET { + self.offset = offset + } else if whence == os.SEEK_CUR { + self.offset += offset + } + return self.offset, nil +} + +func (self *VHDXFile) LStat() (accessors.FileInfo, error) { + return nil, utils.NotImplementedError +} + +// Get a new copy of the handle so it can be seeked independently. +func (self *VHDXFile) _Copy() *VHDXFile { + self.mu.Lock() + defer self.mu.Unlock() + + return &VHDXFile{ + reader: self.reader, + offset: 0, + size: self.size, + } +} + +func GetVHDXImage(full_path *accessors.OSPath, scope vfilter.Scope) ( + zip.ReaderStat, error) { + + pathspec := full_path.PathSpec() + + // The VHDX accessor must use a delegate but if one is not + // provided we use the "auto" accessor, to open the underlying + // file. + if pathspec.DelegateAccessor == "" && pathspec.GetDelegatePath() == "" { + pathspec.DelegatePath = pathspec.Path + pathspec.DelegateAccessor = "auto" + pathspec.Path = "/" + err := full_path.SetPathSpec(pathspec) + if err != nil { + return nil, err + } + } + + accessor, err := accessors.GetAccessor(pathspec.DelegateAccessor, scope) + if err != nil { + scope.Log("vhdx: %v: did you provide a DelegateAccessor PathSpec?", err) + return nil, err + } + + return getCachedVHDXFile(full_path, accessor, scope) +} + +func init() { + accessors.Register(accessors.DescribeAccessor( + zip.NewGzipFileSystemAccessor( + accessors.MustNewLinuxOSPath(""), GetVHDXImage), + accessors.AccessorDescriptor{ + Name: "vhdx", + Description: `Allow reading a VHDX file.`, + })) +} diff --git a/accessors/virtual.go b/accessors/virtual.go new file mode 100644 index 000000000..6137abd21 --- /dev/null +++ b/accessors/virtual.go @@ -0,0 +1,310 @@ +package accessors + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "time" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +type VirtualReadSeekCloser struct { + io.ReadSeeker +} + +func (self VirtualReadSeekCloser) Close() error { + return nil +} + +type VirtualFileInfo struct { + // Only valid when this is not a directory + RawData []byte + + Data_ *ordereddict.Dict + IsDir_ bool + Size_ int64 + + Path *OSPath + Atime_ time.Time + Mtime_ time.Time + Ctime_ time.Time + Btime_ time.Time +} + +func (self *VirtualFileInfo) IsDir() bool { + return self.IsDir_ +} + +func (self *VirtualFileInfo) OSPath() *OSPath { + if self.Path == nil { + return MustNewGenericOSPath("") + } + return self.Path +} + +func (self *VirtualFileInfo) Size() int64 { + if self.Size_ > 0 { + return self.Size_ + } + + return int64(len(self.RawData)) +} + +func (self *VirtualFileInfo) Data() *ordereddict.Dict { + return self.Data_ +} + +func (self *VirtualFileInfo) Name() string { + return self.Path.Basename() +} + +func (self *VirtualFileInfo) Sys() interface{} { + return nil +} + +func (self *VirtualFileInfo) Mode() os.FileMode { + if self.IsDir_ { + return 0755 | os.ModeDir + } + return 0644 +} + +func (self *VirtualFileInfo) ModTime() time.Time { + return self.Mtime_ +} + +func (self *VirtualFileInfo) FullPath() string { + return self.Path.String() +} + +func (self *VirtualFileInfo) Btime() time.Time { + return self.Btime_ +} + +func (self *VirtualFileInfo) Mtime() time.Time { + return self.Mtime_ +} + +func (self *VirtualFileInfo) Ctime() time.Time { + return self.Ctime_ +} + +func (self *VirtualFileInfo) Atime() time.Time { + return self.Atime_ +} + +func (self *VirtualFileInfo) IsLink() bool { + return false +} + +func (self *VirtualFileInfo) Debug() string { + return fmt.Sprintf("%v (%v) %s", self.FullPath(), self.Size(), self.Mode()) +} + +func (self *VirtualFileInfo) GetLink() (*OSPath, error) { + return nil, errors.New("Not implemented") +} + +// Mount tree is very sparse so we dont really need a map here - +// linear search is fast enough. +type directory_node struct { + file_info *VirtualFileInfo + + // Child directory_nodes + children []*directory_node +} + +func (self *directory_node) Debug() string { + res := fmt.Sprintf("directory_node: %v\n", json.MustMarshalString(self.file_info)) + for _, c := range self.children { + res += c.Debug() + } + res += "\n" + return res +} + +func (self *directory_node) GetChild(name string) *directory_node { + for _, c := range self.children { + if c.file_info != nil && + c.file_info.Name() == name { + return c + } + } + + return nil +} + +func (self *directory_node) MakeChild(name string) *directory_node { + if name == "" { + return self + } + + for _, c := range self.children { + if c.file_info != nil && + c.file_info.Name() == name { + return c + } + } + + // If we get here there is no child of this name - make it + new_directory_node := &directory_node{ + file_info: &VirtualFileInfo{ + Path: self.file_info.OSPath().Append(name), + IsDir_: true, + }, + } + self.children = append(self.children, new_directory_node) + return new_directory_node +} + +// A Virtual Filsystem stores files and directories in memory. +type VirtualFilesystemAccessor struct { + root_path *OSPath + root directory_node +} + +func (self VirtualFilesystemAccessor) New(scope vfilter.Scope) ( + FileSystemAccessor, error) { + return self, nil +} + +func (self VirtualFilesystemAccessor) Describe() *AccessorDescriptor { + return &AccessorDescriptor{ + Name: "virtual", + Description: "An accessor for virtual mapped filesystems", + } +} + +func (self VirtualFilesystemAccessor) ParsePath(path string) (*OSPath, error) { + return self.root.file_info.OSPath().Parse(path) +} + +func (self VirtualFilesystemAccessor) Lstat(path string) (FileInfo, error) { + os_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(os_path) +} + +func (self VirtualFilesystemAccessor) LstatWithOSPath( + path *OSPath) (FileInfo, error) { + node, err := self.getNode(path) + if err != nil { + return nil, err + } + + return node.file_info, nil +} + +func (self VirtualFilesystemAccessor) ReadDir(path string) ([]FileInfo, error) { + os_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(os_path) +} + +func (self VirtualFilesystemAccessor) ReadDirWithOSPath( + path *OSPath) ([]FileInfo, error) { + + node, err := self.getNode(path) + if err != nil { + return nil, err + } + + result := make([]FileInfo, 0, len(node.children)) + for _, c := range node.children { + result = append(result, c.file_info) + } + + return result, nil +} + +func (self VirtualFilesystemAccessor) Open(path string) ( + ReadSeekCloser, error) { + os_path, err := self.ParsePath(path) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(os_path) +} + +func (self VirtualFilesystemAccessor) OpenWithOSPath(path *OSPath) ( + ReadSeekCloser, error) { + node, err := self.getNode(path) + if err != nil { + return nil, utils.NotFoundError + } + + return VirtualReadSeekCloser{ + ReadSeeker: bytes.NewReader(node.file_info.RawData), + }, nil +} + +func (self VirtualFilesystemAccessor) getNode(path *OSPath) (*directory_node, error) { + node := &self.root + + for _, c := range path.Components { + if c != "" { + next_node := node.GetChild(c) + if next_node == nil { + return nil, fmt.Errorf("While finding %v: Can not find %v: %w", + path, c, utils.NotFoundError) + } + node = next_node + } + } + return node, nil +} + +func (self *VirtualFilesystemAccessor) SetVirtualDirectory( + dir_path *OSPath, file_info *VirtualFileInfo) { + + node := &self.root + + for _, c := range dir_path.Components { + node = node.MakeChild(c) + } + + file_info.Path = dir_path.Copy() + node.file_info = file_info +} + +func (self *VirtualFilesystemAccessor) SetVirtualFileInfo( + file_info *VirtualFileInfo) { + + node := &self.root + + for _, c := range file_info.OSPath().Components { + if c != "" { + node = node.MakeChild(c) + } + } + node.file_info = file_info +} + +func NewVirtualFilesystemAccessor(root_path *OSPath) *VirtualFilesystemAccessor { + return &VirtualFilesystemAccessor{ + root_path: root_path, + root: directory_node{ + file_info: &VirtualFileInfo{ + Path: root_path, + IsDir_: true, + }, + }, + } +} + +func init() { + json.RegisterCustomEncoder(&VirtualFileInfo{}, MarshalGlobFileInfo) +} diff --git a/accessors/virtual_test.go b/accessors/virtual_test.go new file mode 100644 index 000000000..7010e5107 --- /dev/null +++ b/accessors/virtual_test.go @@ -0,0 +1,94 @@ +package accessors + +import ( + "errors" + "io/ioutil" + "os" + "testing" + + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" +) + +func TestVirtualFilesystemAccessor(t *testing.T) { + root_path := MustNewLinuxOSPath("") + fs_accessor := NewVirtualFilesystemAccessor(root_path) + fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/foo/bar/baz"), &VirtualFileInfo{ + IsDir_: true, + }) + fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/foo/bar2/x"), &VirtualFileInfo{ + RawData: []byte("Hello"), + }) + fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/foo/bar2/y"), &VirtualFileInfo{ + RawData: []byte("Goodbye"), + }) + + ls := func(path string) []string { + children, err := fs_accessor.ReadDir(path) + assert.NoError(t, err) + + results := []string{} + for _, c := range children { + results = append(results, c.FullPath()) + } + return results + } + assert.Equal(t, []string{"/foo/bar", "/foo/bar2"}, ls("/foo")) + assert.Equal(t, []string{"/foo/bar/baz"}, ls("/foo/bar")) + assert.Equal(t, []string{"/foo/bar2/x", "/foo/bar2/y"}, ls("/foo/bar2")) + + // Check the file contents + cat := func(path string) string { + fd, err := fs_accessor.Open(path) + assert.NoError(t, err) + + data, err := ioutil.ReadAll(fd) + assert.NoError(t, err) + + return string(data) + } + + assert.Equal(t, "Hello", cat("/foo/bar2/x")) + assert.Equal(t, "Goodbye", cat("/foo/bar2/y")) + + // Check stats + stat := func(path string) FileInfo { + stat, err := fs_accessor.Lstat(path) + assert.NoError(t, err) + return stat + } + + // Interpolated directory + assert.Equal(t, true, stat("/foo").IsDir()) + assert.Equal(t, false, stat("/foo/bar2/y").IsDir()) + assert.Equal(t, true, stat("/foo/bar/baz").IsDir()) + + // Missing files + _, err := fs_accessor.ReadDir("/nosuchfile") + assert.True(t, errors.Is(err, os.ErrNotExist)) +} + +func TestVirtualFileInfo(t *testing.T) { + root_path := MustNewLinuxOSPath("") + fs_accessor := NewVirtualFilesystemAccessor(root_path) + fs_accessor.SetVirtualDirectory( + MustNewLinuxOSPath("/foo/bar/baz"), &VirtualFileInfo{ + IsDir_: true, + }) + + children, err := fs_accessor.ReadDir("/") + assert.NoError(t, err) + + for _, child := range children { + assert.Equal(t, child.Mode().String(), "drwxr-xr-x") + } + + // Check that json marshal works well - there should be some + // additional fields like ModeStr. + goldie.Assert(t, "TestVirtualFileInfo", + json.MustMarshalIndent(children)) +} diff --git a/accessors/vmdk/cache.go b/accessors/vmdk/cache.go new file mode 100644 index 000000000..dc6ea6d5c --- /dev/null +++ b/accessors/vmdk/cache.go @@ -0,0 +1,121 @@ +package vmdk + +import ( + "io" + "sync" + + "github.com/Velocidex/go-vmdk/parser" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +const ( + VMDK_CACHE_TAG = "__VMDK_CACHE" +) + +// Don't bother expiring this until the end of the query. +type vmdkCache struct { + mu sync.Mutex + + cache map[string]*VMDKFile +} + +func (self *vmdkCache) Get(key string) (*VMDKFile, bool) { + self.mu.Lock() + defer self.mu.Unlock() + + r, pres := self.cache[key] + return r, pres +} + +func (self *vmdkCache) Set(key string, r *VMDKFile) { + self.mu.Lock() + defer self.mu.Unlock() + + self.cache[key] = r +} + +func (self *vmdkCache) Close() { + self.mu.Lock() + defer self.mu.Unlock() + + for _, r := range self.cache { + if r.closer != nil { + r.closer() + } + } +} + +func getCachedVMDKFile( + full_path *accessors.OSPath, + accessor accessors.FileSystemAccessor, + scope vfilter.Scope) (*VMDKFile, error) { + + cache, pres := vql_subsystem.CacheGet(scope, VMDK_CACHE_TAG).(*vmdkCache) + if !pres { + cache = &vmdkCache{ + cache: make(map[string]*VMDKFile), + } + // Cache will remain alive for the duration of the query. + err := vql_subsystem.GetRootScope(scope).AddDestructor(cache.Close) + if err != nil { + cache.Close() + return nil, err + } + + vql_subsystem.CacheSet(scope, VMDK_CACHE_TAG, cache) + } + + key := full_path.String() + res, pres := cache.Get(key) + if pres { + // Give a copy of the cache object so it can be seeked + // independently. + return res._Copy(), nil + } + + delegate, err := full_path.Delegate(scope) + if err != nil { + return nil, err + } + + fd, err := accessor.OpenWithOSPath(delegate) + if err != nil { + return nil, err + } + + vmdk_ctx, err := parser.GetVMDKContext( + utils.MakeReaderAtter(fd), 40960, + func(filename string) (reader io.ReaderAt, closer func(), err error) { + full_path := delegate.Dirname().Append(filename) + fd, err := accessor.OpenWithOSPath(full_path) + if err != nil { + return nil, nil, err + } + return utils.MakeReaderAtter(fd), + func() { fd.Close() }, nil + }) + if err != nil { + return nil, err + } + + vmdk_file := &VMDKFile{ + reader: vmdk_ctx, + size: uint64(vmdk_ctx.Size()), + closer: func() { + scope.Log("vmdk: Closing VMDK file %v\n", key) + fd.Close() + }, + } + + cache.Set(key, vmdk_file) + + stats := vmdk_ctx.Stats() + scope.Log("DEBUG:vmdk: Opened VMDK file %v with %v extents: %v\n", + key, len(stats.Extents), json.MustMarshalString(stats)) + + return vmdk_file, nil +} diff --git a/accessors/vmdk/vmdk.go b/accessors/vmdk/vmdk.go new file mode 100644 index 000000000..37929cf7d --- /dev/null +++ b/accessors/vmdk/vmdk.go @@ -0,0 +1,109 @@ +package vmdk + +import ( + "io" + "os" + "sync" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/accessors/zip" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +type VMDKFile struct { + reader io.ReaderAt + + mu sync.Mutex + offset int64 + size uint64 + + closer func() +} + +// Lifetime is managed by the cache +func (self *VMDKFile) Close() error { + return nil +} + +func (self *VMDKFile) Read(buff []byte) (int, error) { + self.mu.Lock() + defer self.mu.Unlock() + + n, err := self.reader.ReadAt(buff, self.offset) + if err != nil { + return 0, err + } + + if n == 0 { + return 0, io.EOF + } + + self.offset += int64(n) + return n, err +} + +func (self *VMDKFile) Seek(offset int64, whence int) (int64, error) { + self.mu.Lock() + defer self.mu.Unlock() + + if whence == os.SEEK_SET { + self.offset = offset + } else if whence == os.SEEK_CUR { + self.offset += offset + } + return self.offset, nil +} + +func (self *VMDKFile) LStat() (accessors.FileInfo, error) { + return nil, utils.NotImplementedError +} + +// Get a new copy of the handle so it can be seeked independently. +func (self *VMDKFile) _Copy() *VMDKFile { + self.mu.Lock() + defer self.mu.Unlock() + + return &VMDKFile{ + reader: self.reader, + offset: 0, + size: self.size, + } +} + +func GetVMDKImage(full_path *accessors.OSPath, scope vfilter.Scope) ( + zip.ReaderStat, error) { + + pathspec := full_path.PathSpec() + + // The VHDX accessor must use a delegate but if one is not + // provided we use the "auto" accessor, to open the underlying + // file. + if pathspec.DelegateAccessor == "" && pathspec.GetDelegatePath() == "" { + pathspec.DelegatePath = pathspec.Path + pathspec.DelegateAccessor = "auto" + pathspec.Path = "/" + err := full_path.SetPathSpec(pathspec) + if err != nil { + return nil, err + } + } + + accessor, err := accessors.GetAccessor(pathspec.DelegateAccessor, scope) + if err != nil { + scope.Log("vmdk: %v: did you provide a DelegateAccessor PathSpec?", err) + return nil, err + } + + return getCachedVMDKFile(full_path, accessor, scope) +} + +func init() { + accessors.Register(accessors.DescribeAccessor( + zip.NewGzipFileSystemAccessor( + accessors.MustNewLinuxOSPath(""), GetVMDKImage), + accessors.AccessorDescriptor{ + Name: "vmdk", + Description: `Allow reading a VMDK file.`, + })) +} diff --git a/accessors/vql_arg_parser.go b/accessors/vql_arg_parser.go new file mode 100644 index 000000000..c860df6e6 --- /dev/null +++ b/accessors/vql_arg_parser.go @@ -0,0 +1,164 @@ +package accessors + +import ( + "context" + "fmt" + "reflect" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter/arg_parser" + "www.velocidex.com/golang/vfilter/types" +) + +// Parse a value into an OSPath. This is used by VQL functions to +// accept an OSPath object from VQL as an argument. If the argument is +// already a *OSPath then we dont need to do anything and we just +// reuse it saving us the effort of serializing and unserializing the +// same thing. We also accept a string path and automatically convert +// it to an OSPath. +func parseOSPath(ctx context.Context, + scope types.Scope, args *ordereddict.Dict, + value interface{}) (interface{}, error) { + + accessor_name := arg_parser.GetStringArg(ctx, scope, args, "accessor") + accessor, err := GetAccessor(accessor_name, scope) + if err != nil { + return nil, err + } + + return ParseOSPath(ctx, scope, accessor, value) +} + +func ParseOSPath(ctx context.Context, + scope types.Scope, accessor FileSystemAccessor, + value interface{}) (*OSPath, error) { + + switch t := value.(type) { + case types.LazyExpr: + return ParseOSPath(ctx, scope, accessor, t.ReduceWithScope(ctx, scope)) + + case types.Materializer: + return ParseOSPath(ctx, scope, accessor, t.Materialize(ctx, scope)) + + case *OSPath: + return t, nil + + case *PathSpec: + root, err := accessor.ParsePath("") + if err != nil { + return accessor.ParsePath(t.String()) + } + return root, root.SetPathSpec(t) + + case PathSpec: + + root, err := accessor.ParsePath("") + if err != nil { + return accessor.ParsePath(t.String()) + } + return root, root.SetPathSpec(&t) + + case api.FSPathSpec: + // Create an OSPath to represent the abstract filestore path. + // Restore the file extension from the filestore abstract + // pathspec. + components := utils.CopySlice(t.Components()) + if len(components) > 0 { + last_idx := len(components) - 1 + components[last_idx] += api.GetExtensionForFilestore(t) + } + res := MustNewFileStorePath("fs:").Append(components...) + + // Store the FSPathSpec in the data for fast retrieval if we + // are passed to the fs accessor (this is commonly the case). + res.Data = t + + return res, nil + + case api.DSPathSpec: + // Create an OSPath to represent the abstract filestore path. + // Restore the file extension from the filestore abstract + // pathspec. + components := utils.CopySlice(t.Components()) + if len(components) > 0 { + last_idx := len(components) - 1 + components[last_idx] += api.GetExtensionForDatastore(t) + } + return MustNewFileStorePath("ds:").Append(components...), nil + + case string: + return accessor.ParsePath(t) + + case []uint8: + return accessor.ParsePath(string(t)) + + default: + result, _ := accessor.ParsePath("") + + // Is it an array? Generic code to handle arrays - just append + // each element together to form a single path. This allows + // joining components directly: + // ["bin", "ls"] or ["/usr/bin", "ls"] + a_value := reflect.Indirect(reflect.ValueOf(value)) + if a_value.Type().Kind() == reflect.Slice { + for idx := 0; idx < a_value.Len(); idx++ { + slice_item := a_value.Index(int(idx)).Interface() + item, err := ParseOSPath(ctx, scope, accessor, slice_item) + if err != nil { + string_item, ok := slice_item.(string) + if ok { + result = result.Append(string_item) + } + continue + } + result = result.Append(item.Components...) + } + return result, nil + } + + // This is a fatal error on the client. + return nil, fmt.Errorf("Expecting a path arg type, not %T", t) + } +} + +func parseOSPathArray(ctx context.Context, + scope types.Scope, args *ordereddict.Dict, + value interface{}) (interface{}, error) { + + result := []*OSPath{} + + a_value := reflect.Indirect(reflect.ValueOf(value)) + if a_value.Type().Kind() == reflect.Slice { + for idx := 0; idx < a_value.Len(); idx++ { + item, err := parseOSPath(ctx, scope, args, + a_value.Index(int(idx)).Interface()) + if err != nil { + continue + } + item_os_path, ok := item.(*OSPath) + if ok { + result = append(result, item_os_path) + } + } + return result, nil + } + + // If the arg is not a slice then treat it as a single ospath. + item, err := parseOSPath(ctx, scope, args, value) + if err != nil { + return nil, err + } + + item_os_path, ok := item.(*OSPath) + if ok { + result = append(result, item_os_path) + } + return result, nil +} + +func init() { + arg_parser.RegisterParser(&OSPath{}, parseOSPath) + arg_parser.RegisterParser([]*OSPath{}, parseOSPathArray) +} diff --git a/accessors/vql_arg_parser_test.go b/accessors/vql_arg_parser_test.go new file mode 100644 index 000000000..e2e27df4b --- /dev/null +++ b/accessors/vql_arg_parser_test.go @@ -0,0 +1,109 @@ +package accessors_test + +import ( + "context" + "testing" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/file_store/path_specs" + "www.velocidex.com/golang/velociraptor/json" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/arg_parser" + + _ "www.velocidex.com/golang/velociraptor/accessors/file" +) + +type testStruct struct { + Path *accessors.OSPath `vfilter:"required,field=path"` + Accessor string `vfilter:"optional,field=accessor"` +} + +type testCases struct { + name string + in vfilter.Any +} + +var testcases = []testCases{ + {name: "Simple Path", + in: []string{ + "Hello", "World", + }}, + {name: "Path With {", + in: []string{ + "Hello", "{this is a test}", + }}, + {name: "FSPathSpec", + in: path_specs.NewUnsafeFilestorePath("Hello", "World")}, + {name: "FSPathSpec With type", + in: path_specs.NewUnsafeFilestorePath("Hello", "World"). + SetType(api.PATH_TYPE_FILESTORE_DOWNLOAD_ZIP)}, + {name: "DSPathSpec", + in: path_specs.NewUnsafeDatastorePath("Hello", "World")}, + + {name: "DSPathSpec With Type", + in: path_specs.NewUnsafeDatastorePath("Hello", "World"). + SetType(api.PATH_TYPE_DATASTORE_PROTO)}, + + {name: "OSPath", + in: accessors.MustNewGenericOSPath("/foo/bar")}, + + {name: "PathSpec", + in: accessors.MustNewGenericOSPath("/foo/bar").PathSpec()}, + + {name: "Serialized PathSpec", + in: `{"Path": "/foo/bar.txt", "Accessor": "zip", "DelegatePath": "/tmp/file.zip", "DelegateAccessor": "file"}`}, + + {name: "Multiple parts of mixed type", + in: []vfilter.Any{accessors.MustNewGenericOSPath("/foo/bar"), "Hello.txt"}}, + + // Just join all parts + {name: "Multiple parts of mixed type", + in: []vfilter.Any{"/root/home", accessors.MustNewGenericOSPath("/foo/bar"), "Hello.txt"}}, + + {name: "Multiple parts of mixed type 2", + in: []vfilter.Any{"/root/home", `{"Path": "/a/b"}`, "Hello.txt"}}, +} + +func TestVQLParsing(t *testing.T) { + config_obj := &config_proto.Config{} + + // To make this test run on Linux and Windows the same we use a + // neutral accessor. + device_manager := accessors.GetDefaultDeviceManager(config_obj).Copy() + device_manager.Register(accessors.DescribeAccessor( + accessors.NewVirtualFilesystemAccessor(accessors.MustNewLinuxOSPath("")), + accessors.AccessorDescriptor{ + Name: "virt", + })) + + ctx := context.Background() + scope := vql_subsystem.MakeScope(). + AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{}). + Set(constants.SCOPE_DEVICE_MANAGER, device_manager), + ) + + result := ordereddict.NewDict() + + for _, testcase := range testcases { + args := ordereddict.NewDict(). + Set("accessor", "virt"). + Set("path", testcase.in) + arg := &testStruct{} + err := arg_parser.ExtractArgsWithContext(ctx, scope, args, arg) + assert.NoError(t, err) + + result.Set(testcase.name, ordereddict.NewDict(). + Set("Components", arg.Path.Components). + Set("PathSpec", arg.Path.PathSpec())) + } + goldie.Assert(t, "TestVQLParsing", json.MustMarshalIndent(result)) +} diff --git a/accessors/winpmem/logger.go b/accessors/winpmem/logger.go new file mode 100644 index 000000000..73d9e7f52 --- /dev/null +++ b/accessors/winpmem/logger.go @@ -0,0 +1,32 @@ +package winpmem + +import ( + "www.velocidex.com/golang/vfilter/types" +) + +type ScopeLogger struct { + scope types.Scope + prefix string + debug bool +} + +func (self *ScopeLogger) Info(format string, args ...interface{}) { + self.scope.Log("INFO:"+self.prefix+format, args...) +} + +func (self *ScopeLogger) Debug(format string, args ...interface{}) { + if self.debug { + self.scope.Debug("DEBUG:"+self.prefix+format, args...) + } +} + +func (self *ScopeLogger) SetDebug() { + self.debug = true +} + +func (self *ScopeLogger) Progress(pages int) {} +func (self *ScopeLogger) SetProgress(pages int) {} + +func NewLogger(scope types.Scope, prefix string) *ScopeLogger { + return &ScopeLogger{scope: scope, prefix: prefix} +} diff --git a/accessors/winpmem/winpmem.go b/accessors/winpmem/winpmem.go new file mode 100644 index 000000000..c88bd7f50 --- /dev/null +++ b/accessors/winpmem/winpmem.go @@ -0,0 +1,201 @@ +//go:build windows && amd64 && cgo +// +build windows,amd64,cgo + +package winpmem + +import ( + "errors" + "fmt" + "os" + "sync" + + "github.com/Velocidex/WinPmem/go-winpmem" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/uploads" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +// An accessor for physical memory. Uses the winpmem driver to gain +// access to the physical memory. + +const ( + PAGE_SIZE = 0x1000 + DeviceName = `\\.\pmem` +) + +type WinpmemReader struct { + *winpmem.Imager + + mu sync.Mutex + offset int64 +} + +func (self *WinpmemReader) Read(buf []byte) (int, error) { + self.mu.Lock() + defer self.mu.Unlock() + + n, err := self.ReadAt(buf, self.offset) + self.offset += int64(n) + + return n, err +} + +func (self *WinpmemReader) Ranges() []uploads.Range { + self.mu.Lock() + defer self.mu.Unlock() + + result := []uploads.Range{} + size := int64(0) + for _, rng := range self.Stats().Run { + // Fill in a sparse range if needed + if int64(rng.BaseAddress) > size { + result = append(result, uploads.Range{ + Offset: int64(size), + Length: int64(rng.BaseAddress) - size, + IsSparse: true, + }) + } + + // Move the pointer past the end of this range. + size = int64(rng.BaseAddress + rng.NumberOfBytes) + + // Add a real data run + result = append(result, uploads.Range{ + Offset: int64(rng.BaseAddress), + Length: int64(rng.NumberOfBytes), + IsSparse: false, + }) + } + return result +} + +func (self *WinpmemReader) Seek(offset int64, whence int) (int64, error) { + self.mu.Lock() + defer self.mu.Unlock() + + switch whence { + case 0: + self.offset = offset + case 1: + self.offset += offset + case 2: + return 0, utils.NotImplementedError + } + + return int64(self.offset), nil +} + +// Keep the process alive in cache for a bit +func (self *WinpmemReader) Close() error { + return nil +} + +func (self WinpmemReader) Stat() (os.FileInfo, error) { + return &accessors.VirtualFileInfo{}, nil +} + +type WinpmemAccessor struct { + scope vfilter.Scope + imager *winpmem.Imager +} + +const _WinpmemAccessorTag = "_WinpmemAccessor" + +func (self WinpmemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + + result_any := vql_subsystem.CacheGet(scope, _WinpmemAccessorTag) + if result_any == nil { + logger := NewLogger(scope, "winpmem accessor: ") + imager, err := winpmem.NewImager(DeviceName, logger) + if err != nil { + return nil, fmt.Errorf("winpmem: unable to load device, ensure to load it with the winpmem() function first: %w", err) + } + + // We only support this mode now - it is the most reliable. + imager.SetMode(winpmem.PMEM_MODE_PTE) + + // Create a new cache in the scope. + result := &WinpmemAccessor{ + scope: scope, + imager: imager, + } + vql_subsystem.CacheSet(scope, _WinpmemAccessorTag, result) + + vql_subsystem.GetRootScope(scope).AddDestructor(func() { + imager.Close() + }) + + return result, nil + } + + return result_any.(*WinpmemAccessor), nil +} + +func (self WinpmemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "winpmem", + Description: `Access physical memory like a file. Any filename will result in a sparse view of physical memory.`, + Permissions: []acls.ACL_PERMISSION{acls.MACHINE_STATE}, + } +} + +func (self WinpmemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewLinuxOSPath(path) +} + +func (self WinpmemAccessor) ReadDir(path string) ([]accessors.FileInfo, error) { + return nil, errors.New("winpmem accessor: Directory operations not supported.") +} + +func (self WinpmemAccessor) ReadDirWithOSPath( + path *accessors.OSPath) ([]accessors.FileInfo, error) { + return nil, errors.New("winpmem accessor: Directory operations not supported.") +} + +func (self WinpmemAccessor) Lstat(filename string) (accessors.FileInfo, error) { + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return &accessors.VirtualFileInfo{ + Path: full_path, + }, nil +} + +func (self WinpmemAccessor) LstatWithOSPath(full_path *accessors.OSPath) ( + accessors.FileInfo, error) { + + return &accessors.VirtualFileInfo{ + Path: full_path, + }, nil +} + +func (self *WinpmemAccessor) Open(filename string) ( + accessors.ReadSeekCloser, error) { + + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +// Open the same imager for all paths +func (self *WinpmemAccessor) OpenWithOSPath( + full_path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + + return &WinpmemReader{ + Imager: self.imager, + }, nil +} + +func init() { + accessors.Register(&WinpmemAccessor{}) +} diff --git a/accessors/zip/accessor.go b/accessors/zip/accessor.go new file mode 100644 index 000000000..1041f112a --- /dev/null +++ b/accessors/zip/accessor.go @@ -0,0 +1,386 @@ +package zip + +import ( + "strings" + "sync" + "time" + + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/third_party/zip" + "www.velocidex.com/golang/velociraptor/utils" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +const ( + ZipFileSystemAccessorTag = "_ZipFS" + ZipFileNoCaseSystemAccessorTag = "_ZipFSNoCase" +) + +var ( + mu sync.Mutex +) + +// The ZipFileSystemAccessor is cached in a singleton cached in the +// root scope. We keep a list of most recently used caches of zip +// files for quick access. +type ZipFileSystemAccessor struct { + fd_cache map[string]*ZipFileCache + scope vfilter.Scope + nocase bool +} + +func (self *ZipFileSystemAccessor) Copy( + scope vfilter.Scope) *ZipFileSystemAccessor { + mu.Lock() + defer mu.Unlock() + + return &ZipFileSystemAccessor{ + fd_cache: self.fd_cache, + nocase: self.nocase, + scope: scope, + } +} + +// Try to remove any file caches with no references. +func (self *ZipFileSystemAccessor) Trim() { + mu.Lock() + defer mu.Unlock() + + cache_size := vql_subsystem.GetIntFromRow( + self.scope, self.scope, constants.ZIP_FILE_CACHE_SIZE) + if cache_size == 0 { + cache_size = 5 + } + + // Grow the cache up to max 10 elements. + for key, fd := range self.fd_cache { + if fd == nil { + continue + } + + if uint64(len(self.fd_cache)) > cache_size { + fd.mu.Lock() + refs := fd.refs + fd.mu.Unlock() + + if refs == 1 { + fd.Close() + } + } + + // Trim closed fd from our cache. + if fd.IsClosed() { + delete(self.fd_cache, key) + } + } +} + +// Close all the items - called when root scope destroys +func (self *ZipFileSystemAccessor) CloseAll() { + mu.Lock() + defer mu.Unlock() + + // Close all elements + for key, fd := range self.fd_cache { + if fd != nil { + fd.Close() + } + delete(self.fd_cache, key) + } +} + +func (self *ZipFileSystemAccessor) getCachedZipFile(cache_key string) ( + *ZipFileCache, error) { + + zip_file_cache, pres := self.fd_cache[cache_key] + + // The cached value is valid and ready - return it + if pres && + zip_file_cache != nil && + !zip_file_cache.IsClosed() { + zip_file_cache.IncRef() + return zip_file_cache, nil + } + + // Store a nil in the map as a place holder, while we + // build something. + if !pres { + self.fd_cache[cache_key] = nil + return nil, nil + } + + return nil, utils.NotFoundError +} + +// Returns a ZipFileCache wrapper around the zip.Reader. Be sure to +// close it when done. When the query completes, the zip file will be +// closed. +func _GetZipFile(self *ZipFileSystemAccessor, + full_path *accessors.OSPath) (result *ZipFileCache, err error) { + + pathspec := full_path.PathSpec() + + base_pathspec := accessors.PathSpec{ + DelegateAccessor: pathspec.DelegateAccessor, + DelegatePath: pathspec.GetDelegatePath(), + } + cache_key := base_pathspec.String() + full_path.DescribeType() + + for { + mu.Lock() + zip_file_cache, err := self.getCachedZipFile(cache_key) + if err == nil { + // This means the zip file cache needs to be built - we + // are still holding the lock and will release it below. + if zip_file_cache == nil { + mu.Unlock() + break + } + mu.Unlock() + return zip_file_cache, nil + } + mu.Unlock() + time.Sleep(time.Millisecond) + } + + defer func() { + if err != nil { + mu.Lock() + defer mu.Unlock() + delete(self.fd_cache, cache_key) + } + }() + + accessor, err := accessors.GetAccessor( + pathspec.DelegateAccessor, self.scope) + if err != nil { + self.scope.Log("ZipFileSystemAccessor: %v", err) + return nil, err + } + + filename := pathspec.GetDelegatePath() + fd, err := accessor.Open(filename) + if err != nil { + return nil, err + } + + stat, err := accessor.Lstat(filename) + if err != nil { + self.scope.Log("ZipFileSystemAccessor: %v", err) + return nil, err + } + + reader_atter := utils.MakeReaderAtter(fd) + zip_file, err := zip.NewReader(reader_atter, stat.Size()) + if err != nil { + return nil, err + } + + zipAccessorCurrentOpened.Inc() + + // Initial reference of 1 will be closed on scope destructor. + zipAccessorCurrentReferences.Inc() + zipAccessorTotalOpened.Inc() + + zip_file_cache := &ZipFileCache{ + zip_file: zip_file, + fd: fd, + id: utils.GetId(), + + // One reference to the scope. + refs: 1, + zip_file_name: base_pathspec.GetDelegatePath(), + scope: self.scope, + } + + for _, i := range zip_file.File { + if strings.HasPrefix(i.Name, "{") { + continue + } + + // Ignore directories which are signified by a + // trailing / and have no content. + if strings.HasSuffix(i.Name, "/") && i.UncompressedSize64 == 0 { + continue + } + + // Prepare the pathspec for each zip member. In order to + // access the members, we need to open the current zipfile (in + // full_path) and open i.Name as the path. + + // So if the zip has has a pathspec like: + // {DelegateAccessor: "auto", DelegatePath: "path/to/zip"} + + // We need to parse the zip members (unescaping as needed into + // components), and append those components to the zip + // pathspec to get at the member pathspec. + // + // {DelegateAccessor: "auto", DelegatePath: "path/to/zip", + // Path: "zip_escaped_component_list"} + next_path := full_path.Copy() + + // Parse the i.Name as an encoded zip file. Some Zip accessors + // (e.g. the collector accessor) use a special path + // manipulator that allows any path to be represented in a zip + // file by encoding unrepresentable characters. + zip_path, err := full_path.Parse(i.Name) + if err != nil { + continue + } + next_path.Components = zip_path.Components + + next_item := _CDLookup{ + full_path: next_path, + member_file: i, + } + zip_file_cache.lookup = append(zip_file_cache.lookup, next_item) + } + + // Set the new zip cache tracker in the fd cache. + tracker.Inc(zip_file_cache.zip_file_name) + + // Leaking a zip file from this function, increase its reference - + // callers have to close it. + zip_file_cache.IncRef() + + // Replace the nil in the fd_cache with the real fd cache. + mu.Lock() + self.fd_cache[cache_key] = zip_file_cache + mu.Unlock() + + return zip_file_cache, nil +} + +func (self *ZipFileSystemAccessor) Lstat(file_path string) ( + accessors.FileInfo, error) { + full_path, err := self.ParsePath(file_path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +func (self *ZipFileSystemAccessor) LstatWithOSPath( + full_path *accessors.OSPath) (accessors.FileInfo, error) { + + root, err := _GetZipFile(self, full_path) + if err != nil { + return nil, err + } + defer root.Close() + + return root.GetZipInfo(full_path, self.nocase) +} + +func (self *ZipFileSystemAccessor) Open( + filename string) (accessors.ReadSeekCloser, error) { + + full_path, err := self.ParsePath(filename) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self *ZipFileSystemAccessor) OpenWithOSPath( + full_path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + + // Fetch the zip file from cache again. + zip_file_cache, err := _GetZipFile(self, full_path) + if err != nil { + return nil, err + } + defer zip_file_cache.Close() + + // Get the zip member from the zip file. + return zip_file_cache.Open(full_path, self.nocase) +} + +func (self *ZipFileSystemAccessor) ReadDir( + file_path string) ([]accessors.FileInfo, error) { + + full_path, err := self.ParsePath(file_path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(full_path) +} + +func (self *ZipFileSystemAccessor) ReadDirWithOSPath( + full_path *accessors.OSPath) ([]accessors.FileInfo, error) { + + root, err := _GetZipFile(self, full_path) + if err != nil { + return nil, err + } + defer root.Close() + + children, err := root.GetChildren(full_path, self.nocase) + if err != nil { + return nil, err + } + + result := []accessors.FileInfo{} + for _, item := range children { + result = append(result, item) + } + + return result, nil +} + +// Zip files typically use standard / path separators. +func (self ZipFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewGenericOSPath(path) +} + +func (self ZipFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "zip", + Description: `Open a zip file as if it was a directory.`, + + // Doent need special permissions as we open the delegate + Permissions: []acls.ACL_PERMISSION{}, + } +} + +func (self *ZipFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + tag := ZipFileSystemAccessorTag + if self.nocase { + tag = ZipFileNoCaseSystemAccessorTag + } + + result_any := vql_subsystem.CacheGet(scope, tag) + if result_any == nil { + // Create a new cache in the scope. + result := &ZipFileSystemAccessor{ + fd_cache: make(map[string]*ZipFileCache), + scope: scope, + nocase: self.nocase, + } + vql_subsystem.CacheSet(scope, tag, result) + + err := vql_subsystem.GetRootScope(scope).AddDestructor(func() { + result.CloseAll() + }) + if err != nil { + result.CloseAll() + return nil, err + } + + return result, nil + } + + // Make a copy of the filesystem capturing the new scope. + res := result_any.(*ZipFileSystemAccessor) + res.Trim() + + return res.Copy(scope), nil +} diff --git a/accessors/zip/fixtures/TestReferenceCount.golden b/accessors/zip/fixtures/TestReferenceCount.golden new file mode 100644 index 000000000..49a4774b5 --- /dev/null +++ b/accessors/zip/fixtures/TestReferenceCount.golden @@ -0,0 +1,22 @@ +[ + { + "Base": "/hello.txt", + "Data": "hello\n" + }, + { + "Base": "/hello1.txt", + "Data": "hello1\n" + }, + { + "Base": "/hello2.txt", + "Data": "hello2\n" + }, + { + "Base": "/hello3.txt", + "Data": "hello3\n" + }, + { + "Base": "/hello4.txt", + "Data": "hello4\n" + } +] \ No newline at end of file diff --git a/accessors/zip/fixtures/TestReferenceCountNested.golden b/accessors/zip/fixtures/TestReferenceCountNested.golden new file mode 100644 index 000000000..fe8deb392 --- /dev/null +++ b/accessors/zip/fixtures/TestReferenceCountNested.golden @@ -0,0 +1,102 @@ +[ + { + "Base": "/hello.txt", + "Data": "hello\n" + }, + { + "Base": "/hello1.txt", + "Data": "hello1\n" + }, + { + "Base": "/hello2.txt", + "Data": "hello2\n" + }, + { + "Base": "/hello3.txt", + "Data": "hello3\n" + }, + { + "Base": "/hello4.txt", + "Data": "hello4\n" + }, + { + "Base": "/hello.txt", + "Data": "hello\n" + }, + { + "Base": "/hello1.txt", + "Data": "hello1\n" + }, + { + "Base": "/hello2.txt", + "Data": "hello2\n" + }, + { + "Base": "/hello3.txt", + "Data": "hello3\n" + }, + { + "Base": "/hello4.txt", + "Data": "hello4\n" + }, + { + "Base": "/hello.txt", + "Data": "hello\n" + }, + { + "Base": "/hello1.txt", + "Data": "hello1\n" + }, + { + "Base": "/hello2.txt", + "Data": "hello2\n" + }, + { + "Base": "/hello3.txt", + "Data": "hello3\n" + }, + { + "Base": "/hello4.txt", + "Data": "hello4\n" + }, + { + "Base": "/hello.txt", + "Data": "hello\n" + }, + { + "Base": "/hello1.txt", + "Data": "hello1\n" + }, + { + "Base": "/hello2.txt", + "Data": "hello2\n" + }, + { + "Base": "/hello3.txt", + "Data": "hello3\n" + }, + { + "Base": "/hello4.txt", + "Data": "hello4\n" + }, + { + "Base": "/hello.txt", + "Data": "hello\n" + }, + { + "Base": "/hello1.txt", + "Data": "hello1\n" + }, + { + "Base": "/hello2.txt", + "Data": "hello2\n" + }, + { + "Base": "/hello3.txt", + "Data": "hello3\n" + }, + { + "Base": "/hello4.txt", + "Data": "hello4\n" + } +] \ No newline at end of file diff --git a/accessors/zip/gzip.go b/accessors/zip/gzip.go new file mode 100644 index 000000000..63bf508b6 --- /dev/null +++ b/accessors/zip/gzip.go @@ -0,0 +1,353 @@ +/* + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +// A GZip accessor. + +// This accessor provides access to compressed archives. The filename +// is encoded in such a way that this accessor can delegate to another +// accessor to actually open the underlying zip file. This makes it +// possible to open zip files read through e.g. raw ntfs. + +// For example a filename is URL encoded as: +// ntfs:/c:\\Windows\\File.gz + +// Refers to the file opened by the accessor "ntfs" (The URL Scheme) +// with a path (URL Path) of c:\\Windows\File.gz. + +package zip + +import ( + "compress/bzip2" + "compress/gzip" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "time" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/vfilter" +) + +type GzipFileInfo struct { + _modtime time.Time + _name string + _full_path *accessors.OSPath +} + +func (self *GzipFileInfo) IsDir() bool { + return false +} + +func (self *GzipFileInfo) Size() int64 { + // We dont really know the size. + return -1 +} + +func (self *GzipFileInfo) Data() *ordereddict.Dict { + result := ordereddict.NewDict() + return result +} + +func (self *GzipFileInfo) Name() string { + return self._name +} + +func (self *GzipFileInfo) Mode() os.FileMode { + return 0644 +} + +func (self *GzipFileInfo) ModTime() time.Time { + return self._modtime +} + +func (self *GzipFileInfo) FullPath() string { + return self._full_path.String() +} + +func (self *GzipFileInfo) OSPath() *accessors.OSPath { + return self._full_path.Copy() +} + +func (self *GzipFileInfo) Mtime() time.Time { + return self._modtime +} + +func (self *GzipFileInfo) Btime() time.Time { + return self._modtime +} + +func (self *GzipFileInfo) Ctime() time.Time { + return self._modtime +} + +func (self *GzipFileInfo) Atime() time.Time { + return self._modtime +} + +// Not supported +func (self *GzipFileInfo) IsLink() bool { + return false +} + +func (self *GzipFileInfo) GetLink() (*accessors.OSPath, error) { + return nil, errors.New("Not implemented") +} + +type ReaderStat interface { + accessors.ReadSeekCloser + LStat() (accessors.FileInfo, error) +} + +type GzipFileSystemAccessor struct { + scope vfilter.Scope + getter FileGetter + + root *accessors.OSPath +} + +func (self *GzipFileSystemAccessor) Lstat(file_path string) ( + accessors.FileInfo, error) { + + full_path, err := self.ParsePath(file_path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +func (self *GzipFileSystemAccessor) LstatWithOSPath( + file_path *accessors.OSPath) ( + accessors.FileInfo, error) { + seekablegzip, err := self.getter(file_path, self.scope) + if err != nil { + return nil, err + } + defer seekablegzip.Close() + + return seekablegzip.LStat() +} + +func (self *GzipFileSystemAccessor) Open(file_path string) ( + accessors.ReadSeekCloser, error) { + full_path, err := self.ParsePath(file_path) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self *GzipFileSystemAccessor) OpenWithOSPath(path *accessors.OSPath) ( + accessors.ReadSeekCloser, error) { + return self.getter(path, self.scope) +} + +func (self *GzipFileSystemAccessor) ReadDir(file_path string) ( + []accessors.FileInfo, error) { + return nil, nil +} + +func (self *GzipFileSystemAccessor) ReadDirWithOSPath( + full_path *accessors.OSPath) ([]accessors.FileInfo, error) { + return nil, nil +} + +func (self GzipFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return self.root.Parse(path) +} + +func (self GzipFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "gzip", + Description: `Access the content of gzip files. The filename is a pathspec with a delegate accessor opening the actual gzip file.`, + } +} + +func (self GzipFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + return &GzipFileSystemAccessor{ + scope: scope, + getter: self.getter, + root: self.root, + }, nil +} + +func NewGzipFileSystemAccessor( + root *accessors.OSPath, getter FileGetter) *GzipFileSystemAccessor { + return &GzipFileSystemAccessor{root: root, getter: getter} +} + +type SeekableGzip struct { + reader io.ReadCloser + gz io.ReadCloser + info *GzipFileInfo + offset int64 +} + +func (self *SeekableGzip) Close() error { + self.gz.Close() + return self.reader.Close() +} + +func (self *SeekableGzip) Read(buff []byte) (int, error) { + n, err := self.gz.Read(buff) + self.offset += int64(n) + return n, err +} + +func (self *SeekableGzip) Seek(offset int64, whence int) (int64, error) { + switch whence { + case io.SeekStart: + if offset == 0 && self.offset == 0 { + return 0, nil + } + + } + return 0, fmt.Errorf( + "Seeking to %v (%v) not supported on compressed files.", + offset, whence) +} + +func (self *SeekableGzip) LStat() (accessors.FileInfo, error) { + return self.info, nil +} + +// Any getter that implements this can be used +type FileGetter func(full_path *accessors.OSPath, + scope vfilter.Scope) (ReaderStat, error) + +func GetBzip2File(full_path *accessors.OSPath, scope vfilter.Scope) ( + ReaderStat, error) { + pathspec := full_path.PathSpec() + + // The gzip accessor must use a delegate but if one is not + // provided we use the "auto" accessor, to open the underlying + // file. + if pathspec.DelegateAccessor == "" && pathspec.DelegatePath == "" { + pathspec.DelegatePath = pathspec.Path + pathspec.DelegateAccessor = "auto" + } + + accessor, err := accessors.GetAccessor(pathspec.DelegateAccessor, scope) + if err != nil { + scope.Log("%v: did you provide a URL or PathSpec?", err) + return nil, err + } + + delegate_path := pathspec.GetDelegatePath() + fd, err := accessor.Open(delegate_path) + if err != nil { + return nil, err + } + + stat, err := accessor.Lstat(delegate_path) + if err != nil { + return nil, err + } + + zr := bzip2.NewReader(fd) + return &SeekableGzip{reader: fd, + gz: ioutil.NopCloser(zr), + info: &GzipFileInfo{ + _modtime: stat.ModTime(), + _name: stat.Name(), + _full_path: full_path.Copy(), + }}, nil +} + +func GetGzipFile(full_path *accessors.OSPath, scope vfilter.Scope) (ReaderStat, error) { + pathspec := full_path.PathSpec() + + // The gzip accessor must use a delegate but if one is not + // provided we use the "auto" accessor, to open the underlying + // file. + if pathspec.DelegateAccessor == "" && pathspec.GetDelegatePath() == "" { + pathspec.DelegatePath = pathspec.Path + pathspec.DelegateAccessor = "auto" + } + + accessor, err := accessors.GetAccessor(pathspec.DelegateAccessor, scope) + if err != nil { + scope.Log("%v: did you provide a PathSpec?", err) + return nil, err + } + + delegate_path := pathspec.GetDelegatePath() + fd, err := accessor.Open(delegate_path) + if err != nil { + return nil, err + } + + stat, err := accessor.Lstat(delegate_path) + if err != nil { + return nil, err + } + + zr, err := gzip.NewReader(fd) + if err != nil { + // Try to seek the file back + _, err = fd.Seek(0, io.SeekStart) + if err != nil { + // If it does not work - reopen the file. + fd.Close() + fd, err = accessor.Open(pathspec.GetDelegatePath()) + if err != nil { + return nil, err + } + } + + // Not a gzip file but we open it anyway. + return &SeekableGzip{reader: fd, + gz: fd, + info: &GzipFileInfo{ + _modtime: stat.ModTime(), + _name: stat.Name(), + _full_path: full_path.Copy(), + }}, nil + } + + return &SeekableGzip{reader: fd, + gz: zr, + info: &GzipFileInfo{ + _modtime: zr.ModTime, + _name: stat.Name(), + _full_path: full_path.Copy(), + }}, nil +} + +func init() { + accessors.Register(NewGzipFileSystemAccessor( + accessors.MustNewLinuxOSPath(""), GetGzipFile), + ) + + accessors.Register(accessors.DescribeAccessor( + NewGzipFileSystemAccessor( + accessors.MustNewLinuxOSPath(""), GetBzip2File), + accessors.AccessorDescriptor{ + Name: "bzip2", + Description: `Access the content of gzip files. The filename is a pathspec with a delegate accessor opening the actual gzip file.`, + })) + + json.RegisterCustomEncoder(&GzipFileInfo{}, accessors.MarshalGlobFileInfo) +} diff --git a/accessors/zip/gzip_test.go b/accessors/zip/gzip_test.go new file mode 100644 index 000000000..e867f96b3 --- /dev/null +++ b/accessors/zip/gzip_test.go @@ -0,0 +1,56 @@ +package zip + +import ( + "io/ioutil" + "log" + "os" + "path/filepath" + "testing" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/accessors" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + + _ "www.velocidex.com/golang/velociraptor/accessors/file" + _ "www.velocidex.com/golang/velociraptor/accessors/ntfs" +) + +func TestAccessorGzip(t *testing.T) { + scope := vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{})) + scope.SetLogger(log.New(os.Stderr, " ", 0)) + + gzip_accessor, err := accessors.GetAccessor("gzip", scope) + assert.NoError(t, err) + + abs_path, _ := filepath.Abs("../../artifacts/testdata/files/hi.gz") + + fd, err := gzip_accessor.Open(abs_path) + assert.NoError(t, err) + + data, err := ioutil.ReadAll(fd) + assert.NoError(t, err) + + assert.Equal(t, "hello world\n", string(data)) +} + +func TestAccessorBzip2(t *testing.T) { + scope := vql_subsystem.MakeScope().AppendVars(ordereddict.NewDict(). + Set(vql_subsystem.ACL_MANAGER_VAR, acl_managers.NullACLManager{})) + scope.SetLogger(log.New(os.Stderr, " ", 0)) + + gzip_accessor, err := accessors.GetAccessor("bzip2", scope) + assert.NoError(t, err) + + abs_path, _ := filepath.Abs("../../artifacts/testdata/files/goodbye.bz2") + + fd, err := gzip_accessor.Open(abs_path) + assert.NoError(t, err) + + data, err := ioutil.ReadAll(fd) + assert.NoError(t, err) + + assert.Equal(t, "goodbye world\n", string(data)) +} diff --git a/accessors/zip/me.go b/accessors/zip/me.go new file mode 100644 index 000000000..bc7857113 --- /dev/null +++ b/accessors/zip/me.go @@ -0,0 +1,188 @@ +package zip + +import ( + "io" + + "github.com/go-errors/errors" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/config" + "www.velocidex.com/golang/velociraptor/third_party/zip" + "www.velocidex.com/golang/vfilter" +) + +type MEFileSystemAccessor struct { + *ZipFileSystemAccessor +} + +// file_path refers to the fragment - we always use the current exe as +// the root. +func (self *MEFileSystemAccessor) GetZipFile(file_path *accessors.OSPath) ( + *ZipFileCache, error) { + + me := config.EmbeddedFile + + mu.Lock() + zip_file_cache, pres := self.fd_cache[me] + mu.Unlock() + + if !pres { + accessor, err := accessors.GetAccessor("file", self.scope) + if err != nil { + return nil, err + } + + fd, err := accessor.Open(me) + if err != nil { + return nil, err + } + + reader, ok := fd.(io.ReaderAt) + if !ok { + return nil, errors.New("file is not seekable") + } + + stat, err := accessor.Lstat(me) + if err != nil { + return nil, err + } + + zip_file, err := zip.NewReader(reader, stat.Size()) + if err != nil { + return nil, err + } + + zip_file_cache = &ZipFileCache{ + zip_file: zip_file, + fd: fd, + refs: 1, + } + + mu.Lock() + self.fd_cache[me] = zip_file_cache + + for _, i := range zip_file.File { + full_path, err := accessors.NewLinuxOSPath(i.Name) + if err != nil { + continue + } + + zip_file_cache.lookup = append(zip_file_cache.lookup, + _CDLookup{ + full_path: full_path, + member_file: i, + }) + } + mu.Unlock() + } + + zip_file_cache.IncRef() + + return zip_file_cache, nil +} + +func (self *MEFileSystemAccessor) Lstat(serialized_path string) ( + accessors.FileInfo, error) { + full_path, err := self.ParsePath(serialized_path) + if err != nil { + return nil, err + } + + return self.LstatWithOSPath(full_path) +} + +func (self *MEFileSystemAccessor) LstatWithOSPath( + full_path *accessors.OSPath) (accessors.FileInfo, error) { + + root, err := self.GetZipFile(full_path) + if err != nil { + return nil, err + } + + return root.GetZipInfo(full_path, false) +} + +func (self *MEFileSystemAccessor) Open(serialized_path string) ( + accessors.ReadSeekCloser, error) { + // Fetch the zip file from cache again. + full_path, err := self.ParsePath(serialized_path) + if err != nil { + return nil, err + } + + return self.OpenWithOSPath(full_path) +} + +func (self *MEFileSystemAccessor) OpenWithOSPath( + full_path *accessors.OSPath) (accessors.ReadSeekCloser, error) { + + zip_file_cache, err := self.GetZipFile(full_path) + if err != nil { + return nil, err + } + + // Get the zip member from the zip file. + fd, err := zip_file_cache.Open(full_path, false) + if err != nil { + zip_file_cache.Close() + return nil, err + } + return fd, nil +} + +func (self *MEFileSystemAccessor) ReadDir(file_path string) ( + []accessors.FileInfo, error) { + + full_path, err := self.ParsePath(file_path) + if err != nil { + return nil, err + } + + return self.ReadDirWithOSPath(full_path) +} + +func (self *MEFileSystemAccessor) ReadDirWithOSPath( + full_path *accessors.OSPath) ([]accessors.FileInfo, error) { + + root, err := self.GetZipFile(full_path) + if err != nil { + return nil, err + } + + children, err := root.GetChildren(full_path, false) + if err != nil { + return nil, err + } + + result := []accessors.FileInfo{} + for _, item := range children { + result = append(result, item) + } + + return result, nil +} + +func (self MEFileSystemAccessor) ParsePath(path string) ( + *accessors.OSPath, error) { + return accessors.NewLinuxOSPath(path) +} + +func (self MEFileSystemAccessor) New(scope vfilter.Scope) ( + accessors.FileSystemAccessor, error) { + base, err := (&ZipFileSystemAccessor{}).New(scope) + if err != nil { + return nil, err + } + return &MEFileSystemAccessor{base.(*ZipFileSystemAccessor)}, nil +} + +func (self MEFileSystemAccessor) Describe() *accessors.AccessorDescriptor { + return &accessors.AccessorDescriptor{ + Name: "me", + Description: `Access files bundled inside the Velociraptor binary itself. This is used for unpacking extra files delivered by the Offline Collector`, + } +} + +func init() { + accessors.Register(&MEFileSystemAccessor{}) + +} diff --git a/accessors/zip/zip.go b/accessors/zip/zip.go new file mode 100644 index 000000000..f8924c758 --- /dev/null +++ b/accessors/zip/zip.go @@ -0,0 +1,759 @@ +/* + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +// A Zip accessor. + +// This accessor provides access to compressed archives. + +package zip + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "strings" + "sync" + "time" + + "github.com/Velocidex/ordereddict" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/services/debug" + "www.velocidex.com/golang/velociraptor/third_party/zip" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/utils/tempfile" + utils_tempfile "www.velocidex.com/golang/velociraptor/utils/tempfile" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/types" +) + +var ( + zipAccessorCurrentOpened = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "accessor_zip_current_open", + Help: "Number of currently opened ZIP files", + }) + + zipAccessorTotalOpened = promauto.NewCounter(prometheus.CounterOpts{ + Name: "accessor_zip_total_open", + Help: "Total Number of opened ZIP files", + }) + + zipAccessorCurrentReferences = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "accessor_zip_current_references", + Help: "Number of currently referenced ZIP files", + }) + + zipAccessorTotalTmpConversions = promauto.NewCounter(prometheus.CounterOpts{ + Name: "accessor_zip_total_tmp_conversions", + Help: "Total Number of opened ZIP files that we converted to tmp files", + }) + + zipAccessorCurrentTmpConversions = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "accessor_zip_current_tmp_conversions", + Help: "Number of currently referenced ZIP files that exist in tmp files.", + }) +) + +var ( + tracker = &Tracker{refs: make(map[string]int)} +) + +type Tracker struct { + mu sync.Mutex + refs map[string]int +} + +func (self *Tracker) Inc(filename string) { + self.mu.Lock() + defer self.mu.Unlock() + + prev, _ := self.refs[filename] + self.refs[filename] = prev + 1 +} + +func (self *Tracker) Debug() string { + self.mu.Lock() + defer self.mu.Unlock() + + return fmt.Sprintf("%v\n", self.refs) +} + +func (self *Tracker) Reset() { + self.mu.Lock() + defer self.mu.Unlock() + + self.refs = make(map[string]int) +} + +func (self *Tracker) Dec(filename string) { + self.mu.Lock() + defer self.mu.Unlock() + + prev, ok := self.refs[filename] + if ok { + prev-- + if prev == 0 { + delete(self.refs, filename) + } else { + self.refs[filename] = prev + } + return + } + + fmt.Printf("ZipTracker: Close of untracked Open: %v\n", filename) +} + +func (self *Tracker) ProfileWriter(ctx context.Context, + scope vfilter.Scope, output_chan chan vfilter.Row) { + + self.mu.Lock() + defer self.mu.Unlock() + + for filename, ref := range self.refs { + output_chan <- ordereddict.NewDict(). + Set("Filename", filename). + Set("ReferenceCount", ref) + } +} + +// Wrapper around zip.File with reference counting. Note that each +// instance is holding a reference to the zip.Reader it came from. We +// also increase references to ZipFileCache to manage its references +type ZipFileInfo struct { + member_file *zip.File + _full_path *accessors.OSPath +} + +func (self *ZipFileInfo) IsDir() bool { + return self.member_file == nil +} + +func (self *ZipFileInfo) Size() int64 { + if self.member_file == nil { + return 0 + } + + return int64(self.member_file.UncompressedSize64) +} + +func (self *ZipFileInfo) Data() *ordereddict.Dict { + result := ordereddict.NewDict() + if self.member_file != nil { + result.Set("CompressedSize", self.member_file.CompressedSize64) + switch self.member_file.Method { + case 0: + result.Set("Method", "stored") + case 8: + result.Set("Method", "zlib") + default: + result.Set("Method", "unknown") + } + } + + return result +} + +func (self *ZipFileInfo) Name() string { + return self._full_path.Basename() +} + +func (self *ZipFileInfo) Mode() os.FileMode { + var result os.FileMode = 0755 + if self.IsDir() { + result |= os.ModeDir + } + return result +} + +func (self *ZipFileInfo) ModTime() time.Time { + if self.member_file != nil { + return self.member_file.Modified + } + return time.Unix(0, 0) +} + +func (self *ZipFileInfo) FullPath() string { + return self._full_path.String() +} + +func (self *ZipFileInfo) OSPath() *accessors.OSPath { + return self._full_path.Copy() +} + +func (self *ZipFileInfo) SetFullPath(full_path *accessors.OSPath) { + self._full_path = full_path +} + +func (self *ZipFileInfo) Mtime() time.Time { + if self.member_file != nil { + return self.member_file.Modified + } + + return time.Time{} +} + +func (self *ZipFileInfo) Ctime() time.Time { + return self.Mtime() +} + +func (self *ZipFileInfo) Btime() time.Time { + return self.Mtime() +} + +func (self *ZipFileInfo) Atime() time.Time { + return self.Mtime() +} + +// Not supported +func (self *ZipFileInfo) IsLink() bool { + return false +} + +func (self *ZipFileInfo) GetLink() (*accessors.OSPath, error) { + return nil, errors.New("Not implemented") +} + +type _CDLookup struct { + full_path *accessors.OSPath + member_file *zip.File +} + +// A Reference counter around zip.Reader. Each zip.File that is +// released to external code via Open() is wrapped by ZipFileInfo and +// the reference count increases. When the references are exhausted +// the reader will be closed as well as its underlying file. +type ZipFileCache struct { + mu sync.Mutex + zip_file *zip.Reader + + // Underlying file - will be closed when the references are zero. + fd accessors.ReadSeekCloser + + is_closed bool + + // Reference counting - all outstanding references to the zip + // file. Make sure to call ZipFileCache.Close() + refs int + + // An alternative lookup structure to fetch a zip.File (which will + // be wrapped by a ZipFileInfo) + lookup []_CDLookup + + zip_file_name string + + last_active time.Time + + id uint64 + + scope vfilter.Scope +} + +func (self *ZipFileCache) isComponentEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + + for i := 0; i < len(b); i++ { + if a[i] != b[i] { + return false + } + } + + return true +} + +func (self *ZipFileCache) isComponentEqualNoCase(a, b []string) bool { + if len(a) != len(b) { + return false + } + + for i := 0; i < len(b); i++ { + if !strings.EqualFold(a[i], b[i]) { + return false + } + } + + return true +} + +func (self *ZipFileCache) maybeGetPassword() string { + password_any, pres := self.scope.Resolve(constants.ZIP_PASSWORDS) + if pres { + switch t := password_any.(type) { + case types.StoredExpression: + password_any = t.Reduce(context.TODO(), self.scope) + + case types.LazyExpr: + password_any = t.ReduceWithScope( + context.TODO(), self.scope) + } + + password, ok := password_any.(string) + if ok { + return password + } + + } + // If not in scope, check context + password_any, ok := self.scope.GetContext(constants.ZIP_PASSWORDS) + if ok { + password, ok := password_any.(string) + if ok { + return password + } + } + return "" +} + +// Open a file within the cache. Find a direct reference to the +// zip.File object, and increase its reference. NOTE: The returned +// object must be closed to decrement the ZipFileCache reference +// count. +func (self *ZipFileCache) Open(full_path *accessors.OSPath, nocase bool) ( + accessors.ReadSeekCloser, error) { + self.mu.Lock() + defer self.mu.Unlock() + + self.last_active = time.Now() + + info, err := self._GetZipInfo(full_path, nocase) + if err != nil { + return nil, err + } + + // If there is no member file then this is a directory. We return + // it successfully but attempting to read from it is not going to + // work. + if info.member_file == nil { + return &DirectoryZipFile{ + path: info._full_path, + }, nil + } + + // Disable stream authentication because the library unpacks the + // entire stream into memory to verify it. In practice, the + // embedded data.zip file provides sufficient authentication + // anyway. See https://github.com/Velocidex/velociraptor/issues/3150 + info.member_file.DeferAuth = true + + fd, err := info.member_file.Open() + if err == zip.ErrPassword { + password := self.maybeGetPassword() + if password != "" { + info.member_file.SetPassword(password) + fd, err = info.member_file.Open() + } + } + + if err != nil { + return nil, fmt.Errorf("While reading %v %s: %w", + utils.DebugString(info.member_file), + full_path.String(), err) + } + + // We are leaking a zip.File out of our cache so we need to + // increase our reference count. + self.refs++ + zipAccessorCurrentReferences.Inc() + return &SeekableZip{ + delegate: fd, + info: info, + + // Use the correct path + full_path: info.OSPath(), + + // We will be closed when done - Leak a reference. + zip_file: self, + }, nil +} + +func (self *ZipFileCache) GetZipInfo(full_path *accessors.OSPath, nocase bool) ( + *ZipFileInfo, error) { + self.mu.Lock() + defer self.mu.Unlock() + + return self._GetZipInfo(full_path, nocase) +} + +// Searches our lookup table of components to zip.File objects, and +// wraps the zip.File object with a ZipFileInfo object. +func (self *ZipFileCache) _GetZipInfo(full_path *accessors.OSPath, nocase bool) ( + *ZipFileInfo, error) { + + eq := self.isComponentEqual + if nocase { + eq = self.isComponentEqualNoCase + } + + full_path_components := full_path.Components + + var subdir *accessors.OSPath + + // This is O(n) but due to the components length check it is very + // fast. + for _, cd_cache := range self.lookup { + cd_components := cd_cache.full_path.Components + if !eq(full_path_components, cd_components) { + if subdir == nil && + len(cd_components) > len(full_path_components) && + eq(full_path_components, + cd_components[:len(full_path_components)]) { + + subdir = full_path.Copy() + } + continue + } + + // This is an exact match - return it. + return &ZipFileInfo{ + member_file: cd_cache.member_file, + // Return the actual correct casing + _full_path: cd_cache.full_path.Copy(), + }, nil + } + + // This is the best we can do - we have a subdir match + if subdir != nil { + return &ZipFileInfo{ + // Return the actual correct casing + _full_path: subdir, + }, nil + } + + return nil, fmt.Errorf("Zip: %w: %v.", + utils.NotFoundError, full_path.String()) +} + +func (self *ZipFileCache) GetChildren( + full_path *accessors.OSPath, nocase bool) ([]*ZipFileInfo, error) { + self.mu.Lock() + defer self.mu.Unlock() + + // Determine if we already emitted this file. + seen := make(map[string]*ZipFileInfo) + + normalizer := func(x string) string { return x } + if nocase { + normalizer = strings.ToLower + } + +loop: + for _, cd_cache := range self.lookup { + cd_components := cd_cache.full_path.Components + if len(cd_components) <= len(full_path.Components) { + continue loop + } + // This breaks if the cd component does not have the same + // prefix as required. + for j, component := range full_path.Components { + if component != cd_cache.full_path.Components[j] { + if !nocase || !strings.EqualFold( + component, cd_cache.full_path.Components[j]) { + continue loop + } + } + } + + // The required directory depth we need. + depth := len(full_path.Components) + if len(cd_components) <= depth { + continue + } + + // Get the part of the path that is at the required depth. + member_name := normalizer(cd_components[depth]) + + // Have we seen this before? + old_result, pres := seen[member_name] + if pres { + // Only show the first real file (Zip files can have many + // data files with the same name). + if old_result.member_file != nil { + continue + } + + if len(cd_cache.full_path.Components) != depth+1 { + continue + } + } + + // It is a file if the components are an exact match. + if len(cd_cache.full_path.Components) == depth+1 { + seen[member_name] = &ZipFileInfo{ + _full_path: cd_cache.full_path.Copy(), + member_file: cd_cache.member_file, + } + + // A directory has no member file + } else { + basename := cd_cache.full_path.Components[depth] + seen[member_name] = &ZipFileInfo{ + _full_path: full_path.Append(basename), + } + } + } + + result := make([]*ZipFileInfo, 0, len(seen)) + for _, v := range seen { + result = append(result, v) + } + + return result, nil +} + +func (self *ZipFileCache) IncRef() { + self.mu.Lock() + defer self.mu.Unlock() + self.refs++ + zipAccessorCurrentReferences.Inc() +} + +func (self *ZipFileCache) CloseFile(full_path string) { + self.Close() +} + +func (self *ZipFileCache) IsClosed() bool { + self.mu.Lock() + defer self.mu.Unlock() + + return self.is_closed +} + +func (self *ZipFileCache) Close() { + self.mu.Lock() + defer self.mu.Unlock() + + self.refs-- + zipAccessorCurrentReferences.Dec() + if self.refs == 0 { + self.fd.Close() + self.is_closed = true + zipAccessorCurrentOpened.Dec() + tracker.Dec(self.zip_file_name) + } +} + +/* +Zip members are normally compressed and therefore not seekable. If + +We read the members sequentially (e.g. for yara scanning or other +sequential parsing), then there is no need to unpack the +file. However, if the callers need to seek within the archive +member we must unpack it to a tempfile. + +This wrapper manages this by wrapping the underlying zip member and +unpacking to a tmpfile automatically depending on usage patterns. +*/ +type SeekableZip struct { + mu sync.Mutex + + delegate io.ReadCloser + info *ZipFileInfo + offset int64 + + full_path *accessors.OSPath + + // Hold a reference to the zip file itself. + zip_file *ZipFileCache + + // If there is a tmp file backing the file, divert all IO to it. + tmp_file_backing *os.File + + closed bool +} + +func (self *SeekableZip) IsSeekable() bool { + return false +} + +func (self *SeekableZip) Close() error { + self.mu.Lock() + defer self.mu.Unlock() + + // Remove the tmpfile now. + if self.tmp_file_backing != nil { + self.tmp_file_backing.Close() + + zipAccessorCurrentTmpConversions.Dec() + err := os.Remove(self.tmp_file_backing.Name()) + utils_tempfile.RemoveTmpFile(self.tmp_file_backing.Name(), err) + } + + err := self.delegate.Close() + self.zip_file.Close() + self.closed = true + return err +} + +func (self *SeekableZip) DebugString() string { + if self.tmp_file_backing != nil { + return fmt.Sprintf("SeekableZip of %v backed on %v", + self.full_path.String(), self.tmp_file_backing.Name()) + } + return fmt.Sprintf("SeekableZip of %v, closed: %v", + self.full_path.String(), self.closed) +} + +func (self *SeekableZip) Read(buff []byte) (int, error) { + self.mu.Lock() + defer self.mu.Unlock() + + return self.read(buff) +} + +func (self *SeekableZip) read(buff []byte) (int, error) { + if self.tmp_file_backing != nil { + nn, err := self.tmp_file_backing.Read(buff) + self.offset += int64(nn) + return nn, err + } + + n, err := self.delegate.Read(buff) + self.offset += int64(n) + return n, err +} + +// Comply with the ReadAt interface. +func (self *SeekableZip) ReadAt(buf []byte, offset int64) (int, error) { + self.mu.Lock() + defer self.mu.Unlock() + + _, err := self.seek(offset, 0) + if err != nil { + return 0, err + } + n, err := self.read(buf) + return n, err +} + +// Copy the member into a tmpfile. +func (self *SeekableZip) createTmpBackup() (err error) { + // Make a fresh reader to the member so we are seeked to the + // start of it. + reader, err := self.zip_file.Open(self.full_path, false) + if err != nil { + return utils.Wrap(io.EOF, err.Error()) + } + defer reader.Close() + + // Create a tmp file to unpack the zip member into + self.tmp_file_backing, err = tempfile.TempFile("zip*.tmp") + if err != nil { + return err + } + utils_tempfile.AddTmpFile(self.tmp_file_backing.Name()) + + zipAccessorCurrentTmpConversions.Inc() + zipAccessorTotalTmpConversions.Inc() + + _, err = io.Copy(self.tmp_file_backing, reader) + if err != nil { + return err + } + + err = self.tmp_file_backing.Close() + if err != nil { + return err + } + + // Reopen the file for reading. + tmp_reader, err := os.Open(self.tmp_file_backing.Name()) + if err != nil { + return err + } + + self.tmp_file_backing = tmp_reader + return nil +} + +func (self *SeekableZip) Seek(offset int64, whence int) (int64, error) { + self.mu.Lock() + defer self.mu.Unlock() + + return self.seek(offset, whence) +} + +func (self *SeekableZip) seek(offset int64, whence int) (int64, error) { + if self.tmp_file_backing != nil { + current_offset, err := self.tmp_file_backing.Seek(offset, whence) + if err != nil { + self.offset = current_offset + } + return current_offset, err + } + + switch whence { + case io.SeekStart: + if offset == 0 && self.offset == 0 { + return 0, nil + } + + } + + err := self.createTmpBackup() + if err != nil { + return 0, err + } + + current_offset, err := self.tmp_file_backing.Seek(offset, whence) + if err != nil { + self.offset = current_offset + } + return current_offset, err +} + +type DirectoryZipFile struct { + path *accessors.OSPath +} + +func (self DirectoryZipFile) Read(buff []byte) (int, error) { + return 0, utils.Wrap(utils.IOError, "read %v: is a directory", self.path.String()) +} + +func (self DirectoryZipFile) Seek(offset int64, whence int) (int64, error) { + return 0, nil +} + +func (self DirectoryZipFile) Close() error { + return nil +} + +func init() { + accessors.Register(&ZipFileSystemAccessor{}) + accessors.Register(accessors.DescribeAccessor( + &ZipFileSystemAccessor{ + nocase: true, + }, accessors.AccessorDescriptor{ + Name: "zip_nocase", + Description: `Open a zip file as if it was a directory. Although zip files are case-sensitive, this accessor behaves case-insensitive`, + })) + + json.RegisterCustomEncoder(&ZipFileInfo{}, accessors.MarshalGlobFileInfo) + + debug.RegisterProfileWriter(debug.ProfileWriterInfo{ + Name: "ZipTracker", + Description: "Reference counting for open Zip files", + ProfileWriter: tracker.ProfileWriter, + Categories: []string{"Global", "VQL", "Plugins"}, + }) +} diff --git a/accessors/zip/zip_test.go b/accessors/zip/zip_test.go new file mode 100644 index 000000000..784275f89 --- /dev/null +++ b/accessors/zip/zip_test.go @@ -0,0 +1,380 @@ +package zip + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/Velocidex/ordereddict" + "github.com/stretchr/testify/suite" + "www.velocidex.com/golang/velociraptor/accessors" + "www.velocidex.com/golang/velociraptor/file_store/test_utils" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/vtesting" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" + "www.velocidex.com/golang/vfilter" + + _ "www.velocidex.com/golang/velociraptor/accessors/data" + _ "www.velocidex.com/golang/velociraptor/accessors/file" + _ "www.velocidex.com/golang/velociraptor/accessors/ntfs" + _ "www.velocidex.com/golang/velociraptor/result_sets/timed" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + _ "www.velocidex.com/golang/velociraptor/vql/common" + _ "www.velocidex.com/golang/velociraptor/vql/filesystem" +) + +type ZipTestSuite struct { + test_utils.TestSuite +} + +// Make sure that reference counting works well +func (self *ZipTestSuite) TestReferenceCount() { + zip_file, _ := filepath.Abs("../../artifacts/testdata/files/hello.zip") + zip_file_pathspec := accessors.PathSpec{ + DelegateAccessor: "file", + DelegatePath: zip_file, + } + snapshot := vtesting.GetMetrics(self.T(), "accessor_zip_") + + rows, err := test_utils.RunQuery(self.ConfigObj, ` +SELECT OSPath.Path AS Base, + read_file(filename=OSPath, length=10, accessor='zip') AS Data +FROM glob(globs=Glob, root=Root, accessor='zip') +WHERE NOT IsDir`, ordereddict.NewDict(). + Set("Root", zip_file_pathspec). + Set("Glob", "**")) + assert.NoError(self.T(), err) + + state := vtesting.GetMetricsDifference(self.T(), "accessor_zip_", snapshot) + + // Zip file must be closed now + value, _ := state.GetInt64("accessor_zip_current_open") + assert.Equal(self.T(), int64(0), value) + value, _ = state.GetInt64("accessor_zip_current_references") + assert.Equal(self.T(), int64(0), value) + + // We opened the zip file exactly once. + value, _ = state.GetInt64("accessor_zip_total_open") + assert.Equal(self.T(), int64(1), value) + + goldie.Assert(self.T(), "TestReferenceCount", json.MustMarshalIndent(rows)) +} + +// Make sure that reference counting works well +func (self *ZipTestSuite) TestReferenceCountNested() { + zip_file, _ := filepath.Abs("../../artifacts/testdata/files/hello.zip") + zip_file_pathspec := accessors.PathSpec{ + DelegateAccessor: "file", + DelegatePath: zip_file, + } + snapshot := vtesting.GetMetrics(self.T(), "accessor_zip_") + + rows, err := test_utils.RunQuery(self.ConfigObj, ` +SELECT * FROM foreach( +row={ + SELECT OSPath.Path AS Base, + read_file(filename=OSPath, length=10, accessor='zip') AS Data + FROM glob(globs=Glob, root=Root, accessor='zip') + WHERE NOT IsDir +}, query={ + SELECT OSPath.Path AS Base, + read_file(filename=OSPath, length=10, accessor='zip') AS Data + FROM glob(globs=Glob, root=Root, accessor='zip') + WHERE NOT IsDir +})`, ordereddict.NewDict(). + Set("Root", zip_file_pathspec). + Set("Glob", "**")) + assert.NoError(self.T(), err) + + state := vtesting.GetMetricsDifference(self.T(), "accessor_zip_", snapshot) + + // Zip file must be closed now + value, _ := state.GetInt64("accessor_zip_current_open") + assert.Equal(self.T(), int64(0), value) + + value, _ = state.GetInt64("accessor_zip_current_references") + assert.Equal(self.T(), int64(0), value) + + // We opened the zip file exactly once. + value, _ = state.GetInt64("accessor_zip_total_open") + assert.Equal(self.T(), int64(1), value) + + goldie.Assert(self.T(), "TestReferenceCountNested", json.MustMarshalIndent(rows)) +} + +// Zip files are cached in the root scope so they can be reused across +// local scopes. This test calls the chain() plugin to open the same +// nested zip file in inside local chain scope 10 times. However, +// since the zip files are cached they will only be opened once. +func (self *ZipTestSuite) TestCachedZip() { + // Read nested ZIP files - the nested.zip contains another zip + // file, hello.zip which in turn contains some txt files. + zip_file, _ := filepath.Abs("../../artifacts/testdata/files/nested.zip") + zip_file_pathspec := accessors.PathSpec{ + DelegateAccessor: "zip", + Delegate: &accessors.PathSpec{ + DelegateAccessor: "file", + DelegatePath: zip_file, + Path: "hello.zip", + }, + Path: "hello1.txt", + } + + snapshot := vtesting.GetMetrics(self.T(), "accessor_zip_") + + // Read some non existant files to check that we close everything + // on error paths. + rows, err := test_utils.RunQuery(self.ConfigObj, ` +LET ZIP_FILE_CACHE_SIZE <= 30 + +SELECT * from chain( +a={ SELECT read_file(accessor="zip", filename=PathSpec) AS Data FROM scope() }, +b={ SELECT read_file(accessor="zip", filename=PathSpec) AS Data FROM scope() }, +c={ SELECT read_file(accessor="zip", filename=PathSpec) AS Data FROM scope() }, +d={ SELECT read_file(accessor="zip", filename=PathSpec) AS Data FROM scope() }, +e={ SELECT read_file(accessor="zip", filename=PathSpec) AS Data FROM scope() }, +f={ SELECT read_file(accessor="zip", filename=PathSpec) AS Data FROM scope() }, +g={ SELECT read_file(accessor="zip", filename=PathSpec) AS Data FROM scope() }, +h={ SELECT read_file(accessor="zip", filename=PathSpec) AS Data FROM scope() }, +i={ SELECT read_file(accessor="zip", filename=PathSpec) AS Data FROM scope() }, +j={ SELECT read_file(accessor="zip", filename=PathSpec) AS Data FROM scope() } +) +`, ordereddict.NewDict(). + Set("PathSpec", zip_file_pathspec)) + assert.NoError(self.T(), err) + + assert.Equal(self.T(), 10, len(rows)) + for i := 0; i < 9; i++ { + data, _ := rows[i].Get("Data") + assert.Equal(self.T(), "hello1\n", data) + } + + // Make sure we dont have any dangling references + state := vtesting.GetMetricsDifference(self.T(), "accessor_zip_", snapshot) + + // Scope is closed - no zip handles are leaking. + value, _ := state.GetInt64("accessor_zip_current_open") + assert.Equal(self.T(), int64(0), value) + + value, _ = state.GetInt64("accessor_zip_current_references") + assert.Equal(self.T(), int64(0), value) + + value, _ = state.GetInt64("accessor_zip_current_tmp_conversions") + assert.Equal(self.T(), int64(0), value) + + // All up we opened two zip files in total since zip files were + // cached.. + value, _ = state.GetInt64("accessor_zip_total_open") + assert.Equal(self.T(), int64(2), value) + + // Make sure we converted one file to tmp file. + value, _ = state.GetInt64("accessor_zip_total_tmp_conversions") + assert.Equal(self.T(), int64(1), value) +} + +func (self *ZipTestSuite) TestCachedZipWithCacheTrim() { + tracker.Reset() + + // Read nested ZIP files - the nested.zip contains another zip + // file, hello.zip which in turn contains some txt files. + zip_file, _ := filepath.Abs("../../artifacts/testdata/files/nested.zip") + + env := ordereddict.NewDict() + for i := 0; i < 11; i++ { + zip_file_pathspec := accessors.PathSpec{ + DelegateAccessor: "zip", + Delegate: &accessors.PathSpec{ + DelegateAccessor: "file", + DelegatePath: zip_file, + Path: fmt.Sprintf("hello%d.zip", i), + }, + Path: "hello1.txt", + } + env.Set(fmt.Sprintf("PathSpec%d", i), zip_file_pathspec) + } + + snapshot := vtesting.GetMetrics(self.T(), "accessor_zip_") + + // Read some non existant files to check that we close everything + // on error paths. Make the zip cache size very small to ensure we + // close all files as we go along.. + rows, err := test_utils.RunQuery(self.ConfigObj, ` +LET ZIP_FILE_CACHE_SIZE <= 3 +SELECT * from chain( +async=TRUE, +a={ SELECT read_file(accessor="zip", filename=PathSpec1) AS Data, PathSpec1 FROM scope() }, +b={ SELECT read_file(accessor="zip", filename=PathSpec2) AS Data, PathSpec2 FROM scope() }, +c={ SELECT read_file(accessor="zip", filename=PathSpec3) AS Data, PathSpec3 FROM scope() }, +d={ SELECT read_file(accessor="zip", filename=PathSpec4) AS Data, PathSpec4 FROM scope() }, +e={ SELECT read_file(accessor="zip", filename=PathSpec5) AS Data, PathSpec5 FROM scope() }, +f={ SELECT read_file(accessor="zip", filename=PathSpec6) AS Data, PathSpec6 FROM scope() }, +g={ SELECT read_file(accessor="zip", filename=PathSpec7) AS Data, PathSpec7 FROM scope() }, +h={ SELECT read_file(accessor="zip", filename=PathSpec8) AS Data, PathSpec8 FROM scope() }, +i={ SELECT read_file(accessor="zip", filename=PathSpec9) AS Data, PathSpec9 FROM scope() }, +j={ SELECT read_file(accessor="zip", filename=PathSpec10) AS Data, PathSpec10 FROM scope() } +) +`, env) + assert.NoError(self.T(), err) + + assert.Equal(self.T(), 10, len(rows)) + for i := 0; i < 9; i++ { + data, _ := rows[i].Get("Data") + assert.Equal(self.T(), "hello1\n", data, "Failed reading %v", rows[i]) + } + + // Make sure we dont have any dangling references + state := vtesting.GetMetricsDifference(self.T(), "accessor_zip_", snapshot) + + // Scope is closed - no zip handles are leaking. + vtesting.WaitUntil(5*time.Second, self.T(), func() bool { + state := vtesting.GetMetricsDifference(self.T(), "accessor_zip_", snapshot) + value, _ := state.GetInt64("accessor_zip_current_open") + + return int64(0) == value + }) + + value, _ := state.GetInt64("accessor_zip_current_references") + assert.Equal(self.T(), int64(0), value) + + value, _ = state.GetInt64("accessor_zip_current_tmp_conversions") + assert.Equal(self.T(), int64(0), value) + + // All up we opened 11 zip files in total (the primary one and + // each embedded zip file. Sometimes due to race conditions we may + // open a file more than once but this is ok as long as it is not + // too much. + value, _ = state.GetInt64("accessor_zip_total_open") + assert.True(self.T(), int64(11) <= value, + "accessor_zip_total_open: %v", value) + + assert.True(self.T(), int64(15) > value, + "accessor_zip_total_open: %v", value) + + // Each nested zip file was extracted to tmpfile. + value, _ = state.GetInt64("accessor_zip_total_tmp_conversions") + assert.Equal(self.T(), int64(10), value) +} + +func (self *ZipTestSuite) TestNoCaseZip() { + // Read nested ZIP files - the nested.zip contains another zip + // file, hello.zip which in turn contains some txt files. + zip_file, _ := filepath.Abs("../../artifacts/testdata/files/hello.zip") + zip_file_pathspec := accessors.PathSpec{ + DelegateAccessor: "file", + DelegatePath: zip_file, + Path: "HeLLo1.TxT", + } + + // Read some non existant files to check that we close everything + // on error paths. + rows, err := test_utils.RunQuery(self.ConfigObj, ` +LET ZIP_FILE_CACHE_SIZE <= 30 + +SELECT read_file(accessor="zip_nocase", filename=PathSpec) AS Data FROM scope() +`, ordereddict.NewDict(). + Set("PathSpec", zip_file_pathspec)) + assert.NoError(self.T(), err) + + assert.Equal(self.T(), 1, len(rows)) + + data, _ := rows[0].Get("Data") + assert.Equal(self.T(), "hello1\n", data) +} + +// Check that transitive access checks are done automatically. We open +// a zip file for a user who does not have FILESYSTEM_READ. While the +// zip accessor does not declare a permission required, the delegate +// does in the case of a file. +func (self *ZipTestSuite) TestPermissions() { + err := services.GrantRoles(self.ConfigObj, "user", []string{"reader"}) + assert.NoError(self.T(), err) + + log_buffer := &strings.Builder{} + + zip_file, _ := filepath.Abs("../../artifacts/testdata/files/hello.zip") + zip_file_pathspec := &accessors.PathSpec{ + DelegateAccessor: "file", + DelegatePath: zip_file, + Path: "hello1.txt", + } + + // Now open a zip file from the data accessor. + fd, err := os.Open(zip_file) + assert.NoError(self.T(), err) + defer fd.Close() + + data, err := ioutil.ReadAll(fd) + assert.NoError(self.T(), err) + + zip_scope_pathspec := &accessors.PathSpec{ + DelegateAccessor: "scope", + DelegatePath: "ZipContents", + Path: "hello1.txt", + } + + zip_data_pathspec := &accessors.PathSpec{ + DelegateAccessor: "data", + DelegatePath: string(data), + Path: "hello1.txt", + } + + builder := services.ScopeBuilder{ + Config: self.ConfigObj, + ACLManager: acl_managers.NewRoleACLManager(self.ConfigObj, "user"), + Env: ordereddict.NewDict(). + Set("PathSpec", zip_file_pathspec). + Set("PathSpecScope", zip_scope_pathspec). + Set("PathSpecData", zip_data_pathspec). + Set("ZipContents", string(data)), + Logger: log.New(log_buffer, "vql: ", 0), + } + + manager, err := services.GetRepositoryManager(self.ConfigObj) + assert.NoError(self.T(), err) + + scope := manager.BuildScope(builder) + defer scope.Close() + + run_query := func(query string) string { + multi_vql, err := vfilter.MultiParse(query) + assert.NoError(self.T(), err) + + for _, vql := range multi_vql { + for row := range vql.Eval(self.Ctx, scope) { + res, _ := scope.Associative(row, "Data") + return res.(string) + } + } + return "" + } + + // Reading from the file accessor is not allowed. + assert.Equal(self.T(), "", + run_query(`SELECT read_file(accessor="zip", filename=PathSpec) AS Data FROM scope()`)) + assert.Contains(self.T(), log_buffer.String(), "Accessor file: PermissionDenied") + + log_buffer.Reset() + + // But it is ok to read from the data or scope accessors. + assert.Equal(self.T(), "hello1\n", + run_query(`SELECT read_file(accessor="zip", filename=PathSpecScope) AS Data FROM scope()`)) + assert.NotContains(self.T(), log_buffer.String(), "PermissionDenied") + + log_buffer.Reset() + + assert.Equal(self.T(), "hello1\n", + run_query(`SELECT read_file(accessor="zip", filename=PathSpecData) AS Data FROM scope()`)) + assert.NotContains(self.T(), log_buffer.String(), "PermissionDenied") + +} + +func TestZipAccessor(t *testing.T) { + suite.Run(t, &ZipTestSuite{}) +} diff --git a/acls/acls.go b/acls/acls.go index 08951e9ec..d1f180928 100644 --- a/acls/acls.go +++ b/acls/acls.go @@ -50,11 +50,6 @@ Tips: import ( "fmt" "strings" - - acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" - config_proto "www.velocidex.com/golang/velociraptor/config/proto" - "www.velocidex.com/golang/velociraptor/datastore" - "www.velocidex.com/golang/velociraptor/paths" ) type ACL_PERMISSION int @@ -62,10 +57,7 @@ type ACL_PERMISSION int const ( NO_PERMISSIONS ACL_PERMISSION = iota - // Issue all queries without restriction - ALL_QUERY - - // Issue any query at all (ALL_QUERY implies ANY_QUERY). + // Issue any query at all. ANY_QUERY // Publish events to server side queues @@ -80,6 +72,14 @@ const ( // Schedule or cancel new collections on clients. COLLECT_CLIENT + // This is a special custom permission which allows the user to + // collect "basic" artifacts. For this to work the administrator + // needs to set the "basic" metadata on the artifact definition. + COLLECT_BASIC + + // Allows the user to start a hunt + START_HUNT + // Schedule new artifact collections on velociraptor servers. COLLECT_SERVER @@ -98,18 +98,30 @@ const ( // Allowed to manage server configuration. SERVER_ADMIN + // Allowed to manage orgs + ORG_ADMIN + + // Allows the user to specify a different username for the query() plugin + IMPERSONATION + // Allowed to read arbitrary files from the filesystem. FILESYSTEM_READ // Allowed to create files on the filesystem. FILESYSTEM_WRITE + // Allowed to make network connections + NETWORK + // Allowed to collect state information from machines (e.g. pslist()). MACHINE_STATE // Allowed to create zip files. PREPARE_RESULTS + // Allowed to delete results from the server + DELETE_RESULTS + // Allowed raw datastore access DATASTORE_ACCESS @@ -117,12 +129,18 @@ const ( // GetRolePermissions and acl.proto ) +func (self ACL_PERMISSION) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf("\"%s\"", self.String())), nil +} + +func (self ACL_PERMISSION) MarshalYAML() (interface{}, error) { + return self.String(), nil +} + func (self ACL_PERMISSION) String() string { switch self { case NO_PERMISSIONS: return "NO_PERMISSIONS" - case ALL_QUERY: - return "ALL_QUERY" case ANY_QUERY: return "ANY_QUERY" case PUBLISH: @@ -133,6 +151,10 @@ func (self ACL_PERMISSION) String() string { return "LABEL_CLIENT" case COLLECT_CLIENT: return "COLLECT_CLIENT" + case COLLECT_BASIC: + return "COLLECT_BASIC" + case START_HUNT: + return "START_HUNT" case COLLECT_SERVER: return "COLLECT_SERVER" case ARTIFACT_WRITER: @@ -145,14 +167,22 @@ func (self ACL_PERMISSION) String() string { return "NOTEBOOK_EDITOR" case SERVER_ADMIN: return "SERVER_ADMIN" + case ORG_ADMIN: + return "ORG_ADMIN" + case IMPERSONATION: + return "IMPERSONATION" case FILESYSTEM_READ: return "FILESYSTEM_READ" case FILESYSTEM_WRITE: return "FILESYSTEM_WRITE" + case NETWORK: + return "NETWORK" case MACHINE_STATE: return "MACHINE_STATE" case PREPARE_RESULTS: return "PREPARE_RESULTS" + case DELETE_RESULTS: + return "DELETE_RESULTS" case DATASTORE_ACCESS: return "DATASTORE_ACCESS" @@ -164,9 +194,6 @@ func GetPermission(name string) ACL_PERMISSION { switch strings.ToUpper(name) { case "NO_PERMISSIONS": return NO_PERMISSIONS - - case "ALL_QUERY": - return ALL_QUERY case "ANY_QUERY": return ANY_QUERY case "PUBLISH": @@ -177,6 +204,10 @@ func GetPermission(name string) ACL_PERMISSION { return LABEL_CLIENT case "COLLECT_CLIENT": return COLLECT_CLIENT + case "COLLECT_BASIC": + return COLLECT_BASIC + case "START_HUNT": + return START_HUNT case "COLLECT_SERVER": return COLLECT_SERVER case "ARTIFACT_WRITER": @@ -189,173 +220,25 @@ func GetPermission(name string) ACL_PERMISSION { return NOTEBOOK_EDITOR case "SERVER_ADMIN": return SERVER_ADMIN + case "ORG_ADMIN": + return ORG_ADMIN + case "IMPERSONATION": + return IMPERSONATION case "FILESYSTEM_READ": return FILESYSTEM_READ case "FILESYSTEM_WRITE": return FILESYSTEM_WRITE + case "NETWORK": + return NETWORK case "MACHINE_STATE": return MACHINE_STATE case "PREPARE_RESULTS": return PREPARE_RESULTS + case "DELETE_RESULTS": + return DELETE_RESULTS case "DATASTORE_ACCESS": return DATASTORE_ACCESS } return NO_PERMISSIONS } - -func GetPolicy( - config_obj *config_proto.Config, - principal string) (*acl_proto.ApiClientACL, error) { - - db, err := datastore.GetDB(config_obj) - if err != nil { - return nil, err - } - - acl_obj := &acl_proto.ApiClientACL{} - user_path_manager := paths.UserPathManager{Name: principal} - err = db.GetSubject(config_obj, user_path_manager.ACL(), acl_obj) - if err != nil { - return nil, err - } - - return acl_obj, nil -} - -// GetEffectivePolicy expands any roles in the policy object to -// produce a simple object. -func GetEffectivePolicy( - config_obj *config_proto.Config, - principal string) (*acl_proto.ApiClientACL, error) { - - db, err := datastore.GetDB(config_obj) - if err != nil { - return nil, err - } - - acl_obj := &acl_proto.ApiClientACL{} - user_path_manager := paths.UserPathManager{Name: principal} - err = db.GetSubject(config_obj, user_path_manager.ACL(), acl_obj) - if err != nil { - return nil, err - } - - err = GetRolePermissions(config_obj, acl_obj.Roles, acl_obj) - if err != nil { - return nil, err - } - - return acl_obj, nil -} - -func SetPolicy( - config_obj *config_proto.Config, - principal string, acl_obj *acl_proto.ApiClientACL) error { - - db, err := datastore.GetDB(config_obj) - if err != nil { - return err - } - - user_path_manager := paths.UserPathManager{Name: principal} - return db.SetSubject(config_obj, user_path_manager.ACL(), acl_obj) -} - -func CheckAccess( - config_obj *config_proto.Config, - principal string, - permissions ...ACL_PERMISSION) (bool, error) { - - // Internal calls from the server are allowed to do anything. - if config_obj.Client != nil && principal == config_obj.Client.PinnedServerName { - return true, nil - } - - if principal == "" { - return false, nil - } - - acl_obj, err := GetEffectivePolicy(config_obj, principal) - if err != nil { - return false, err - } - - for _, permission := range permissions { - ok, err := CheckAccessWithToken(acl_obj, permission) - if !ok || err != nil { - return ok, err - } - } - - return true, nil -} - -func CheckAccessWithToken( - token *acl_proto.ApiClientACL, - permission ACL_PERMISSION, args ...string) (bool, error) { - - // Requested permission - switch permission { - case ALL_QUERY: - return token.AllQuery, nil - - case ANY_QUERY: - return token.AnyQuery, nil - - case PUBLISH: - if len(args) == 1 { - for _, allowed_queue := range token.PublishQueues { - if allowed_queue == args[0] { - return true, nil - } - - } - } - - case READ_RESULTS: - return token.ReadResults, nil - - case LABEL_CLIENT: - return token.LabelClients, nil - - case COLLECT_CLIENT: - return token.CollectClient, nil - - case COLLECT_SERVER: - return token.CollectServer, nil - - case ARTIFACT_WRITER: - return token.ArtifactWriter, nil - - case SERVER_ARTIFACT_WRITER: - return token.ServerArtifactWriter, nil - - case EXECVE: - return token.Execve, nil - - case NOTEBOOK_EDITOR: - return token.NotebookEditor, nil - - case SERVER_ADMIN: - return token.ServerAdmin, nil - - case FILESYSTEM_READ: - return token.FilesystemRead, nil - - case FILESYSTEM_WRITE: - return token.FilesystemWrite, nil - - case MACHINE_STATE: - return token.MachineState, nil - - case PREPARE_RESULTS: - return token.PrepareResults, nil - - case DATASTORE_ACCESS: - return token.DatastoreAccess, nil - - } - - return false, nil -} diff --git a/acls/api.go b/acls/api.go new file mode 100644 index 000000000..5f85fb6e0 --- /dev/null +++ b/acls/api.go @@ -0,0 +1,7 @@ +package acls + +import "www.velocidex.com/golang/velociraptor/utils" + +var ( + PermissionDenied = utils.Wrap(utils.PermissionDenied, "PermissionDenied") +) diff --git a/acls/fixtures/TestMergeACL.golden b/acls/fixtures/TestMergeACL.golden new file mode 100644 index 000000000..1bb3fb468 --- /dev/null +++ b/acls/fixtures/TestMergeACL.golden @@ -0,0 +1,10 @@ +{ + "Merge": { + "collect_server": true, + "execve": true, + "roles": [ + "reader", + "org_admin" + ] + } +} \ No newline at end of file diff --git a/acls/lockdown.go b/acls/lockdown.go new file mode 100644 index 000000000..b78dc036a --- /dev/null +++ b/acls/lockdown.go @@ -0,0 +1,24 @@ +package acls + +import ( + "sync" + + acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" +) + +var ( + mu sync.Mutex + lockdown_token *acl_proto.ApiClientACL +) + +func LockdownToken() *acl_proto.ApiClientACL { + mu.Lock() + defer mu.Unlock() + return lockdown_token +} + +func SetLockdownToken(token *acl_proto.ApiClientACL) { + mu.Lock() + defer mu.Unlock() + lockdown_token = token +} diff --git a/acls/policy.go b/acls/policy.go new file mode 100644 index 000000000..bcd9fa298 --- /dev/null +++ b/acls/policy.go @@ -0,0 +1,73 @@ +package acls + +import ( + "reflect" + "sort" + "strings" + + "github.com/Velocidex/ordereddict" + acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" +) + +func ParsePolicyFromDict(scope vfilter.Scope, in *ordereddict.Dict) ( + result *acl_proto.ApiClientACL, err error) { + + policy_map := in.ToMap() + + result = &acl_proto.ApiClientACL{} + result_value := reflect.Indirect(reflect.ValueOf(result)) + + // Get a list of fields + var valid_fields []string + res_type := result_value.Type() + for i := 0; i < res_type.NumField(); i++ { + field := res_type.Field(i) + if !field.IsExported() { + continue + } + + field_name := strings.Split(field.Tag.Get("json"), ",")[0] + switch field_name { + case "roles": + result.Roles, _ = in.GetStrings(field_name) + valid_fields = append(valid_fields, field_name) + + case "publish_queues": + result.PublishQueues, _ = in.GetStrings(field_name) + valid_fields = append(valid_fields, field_name) + + case "super_user": + + default: + valid_fields = append(valid_fields, field_name) + value, pres := in.Get(field_name) + if !pres { + continue + } + + // Field name is not the same as json name + field_value := result_value.FieldByName(field.Name) + if field.Type.Kind() != reflect.Bool || !field_value.CanSet() { + continue + } + + field_value.SetBool(scope.Bool(value)) + } + + delete(policy_map, field_name) + } + + if len(policy_map) != 0 { + var fields []string + for k := range policy_map { + fields = append(fields, k) + } + sort.Strings(fields) + sort.Strings(valid_fields) + return nil, utils.Wrap(utils.InvalidArgError, "Parsing Policy: Invalid policy fields: %v. Valid fields are %v", fields, valid_fields) + } + + return result, nil +} diff --git a/acls/proto/acl.pb.go b/acls/proto/acl.pb.go index 2d71ccecd..1a9a2d180 100644 --- a/acls/proto/acl.pb.go +++ b/acls/proto/acl.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: acl.proto package proto @@ -26,23 +23,35 @@ type ApiClientACL struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + // if this is set we allow everything - normally only set by + // server-server comms. + SuperUser bool `protobuf:"varint,21,opt,name=super_user,json=superUser,proto3" json:"super_user,omitempty"` AllQuery bool `protobuf:"varint,1,opt,name=all_query,json=allQuery,proto3" json:"all_query,omitempty"` AnyQuery bool `protobuf:"varint,10,opt,name=any_query,json=anyQuery,proto3" json:"any_query,omitempty"` PublishQueues []string `protobuf:"bytes,2,rep,name=publish_queues,json=publishQueues,proto3" json:"publish_queues,omitempty"` ReadResults bool `protobuf:"varint,3,opt,name=read_results,json=readResults,proto3" json:"read_results,omitempty"` LabelClients bool `protobuf:"varint,11,opt,name=label_clients,json=labelClients,proto3" json:"label_clients,omitempty"` CollectClient bool `protobuf:"varint,4,opt,name=collect_client,json=collectClient,proto3" json:"collect_client,omitempty"` + CollectBasic bool `protobuf:"varint,24,opt,name=collect_basic,json=collectBasic,proto3" json:"collect_basic,omitempty"` + StartHunt bool `protobuf:"varint,22,opt,name=start_hunt,json=startHunt,proto3" json:"start_hunt,omitempty"` CollectServer bool `protobuf:"varint,5,opt,name=collect_server,json=collectServer,proto3" json:"collect_server,omitempty"` ArtifactWriter bool `protobuf:"varint,6,opt,name=artifact_writer,json=artifactWriter,proto3" json:"artifact_writer,omitempty"` ServerArtifactWriter bool `protobuf:"varint,15,opt,name=server_artifact_writer,json=serverArtifactWriter,proto3" json:"server_artifact_writer,omitempty"` Execve bool `protobuf:"varint,7,opt,name=execve,proto3" json:"execve,omitempty"` NotebookEditor bool `protobuf:"varint,8,opt,name=notebook_editor,json=notebookEditor,proto3" json:"notebook_editor,omitempty"` - ServerAdmin bool `protobuf:"varint,12,opt,name=server_admin,json=serverAdmin,proto3" json:"server_admin,omitempty"` - FilesystemRead bool `protobuf:"varint,13,opt,name=filesystem_read,json=filesystemRead,proto3" json:"filesystem_read,omitempty"` - FilesystemWrite bool `protobuf:"varint,14,opt,name=filesystem_write,json=filesystemWrite,proto3" json:"filesystem_write,omitempty"` - MachineState bool `protobuf:"varint,16,opt,name=machine_state,json=machineState,proto3" json:"machine_state,omitempty"` - PrepareResults bool `protobuf:"varint,17,opt,name=prepare_results,json=prepareResults,proto3" json:"prepare_results,omitempty"` - DatastoreAccess bool `protobuf:"varint,18,opt,name=datastore_access,json=datastoreAccess,proto3" json:"datastore_access,omitempty"` + // Has the ability to add/remove/list orgs. A user with + // server_admin on the root org will also receive org_admin. + OrgAdmin bool `protobuf:"varint,19,opt,name=org_admin,json=orgAdmin,proto3" json:"org_admin,omitempty"` + // Allows a user to run queries as another user. + Impersonation bool `protobuf:"varint,20,opt,name=impersonation,proto3" json:"impersonation,omitempty"` + ServerAdmin bool `protobuf:"varint,12,opt,name=server_admin,json=serverAdmin,proto3" json:"server_admin,omitempty"` + FilesystemRead bool `protobuf:"varint,13,opt,name=filesystem_read,json=filesystemRead,proto3" json:"filesystem_read,omitempty"` + FilesystemWrite bool `protobuf:"varint,14,opt,name=filesystem_write,json=filesystemWrite,proto3" json:"filesystem_write,omitempty"` + Network bool `protobuf:"varint,25,opt,name=network,proto3" json:"network,omitempty"` + MachineState bool `protobuf:"varint,16,opt,name=machine_state,json=machineState,proto3" json:"machine_state,omitempty"` + PrepareResults bool `protobuf:"varint,17,opt,name=prepare_results,json=prepareResults,proto3" json:"prepare_results,omitempty"` + DeleteResults bool `protobuf:"varint,23,opt,name=delete_results,json=deleteResults,proto3" json:"delete_results,omitempty"` + DatastoreAccess bool `protobuf:"varint,18,opt,name=datastore_access,json=datastoreAccess,proto3" json:"datastore_access,omitempty"` // A list of roles in lieu of the permissions above. These will be // interpolated into this ACL object. Roles []string `protobuf:"bytes,9,rep,name=roles,proto3" json:"roles,omitempty"` @@ -80,6 +89,13 @@ func (*ApiClientACL) Descriptor() ([]byte, []int) { return file_acl_proto_rawDescGZIP(), []int{0} } +func (x *ApiClientACL) GetSuperUser() bool { + if x != nil { + return x.SuperUser + } + return false +} + func (x *ApiClientACL) GetAllQuery() bool { if x != nil { return x.AllQuery @@ -122,6 +138,20 @@ func (x *ApiClientACL) GetCollectClient() bool { return false } +func (x *ApiClientACL) GetCollectBasic() bool { + if x != nil { + return x.CollectBasic + } + return false +} + +func (x *ApiClientACL) GetStartHunt() bool { + if x != nil { + return x.StartHunt + } + return false +} + func (x *ApiClientACL) GetCollectServer() bool { if x != nil { return x.CollectServer @@ -157,6 +187,20 @@ func (x *ApiClientACL) GetNotebookEditor() bool { return false } +func (x *ApiClientACL) GetOrgAdmin() bool { + if x != nil { + return x.OrgAdmin + } + return false +} + +func (x *ApiClientACL) GetImpersonation() bool { + if x != nil { + return x.Impersonation + } + return false +} + func (x *ApiClientACL) GetServerAdmin() bool { if x != nil { return x.ServerAdmin @@ -178,6 +222,13 @@ func (x *ApiClientACL) GetFilesystemWrite() bool { return false } +func (x *ApiClientACL) GetNetwork() bool { + if x != nil { + return x.Network + } + return false +} + func (x *ApiClientACL) GetMachineState() bool { if x != nil { return x.MachineState @@ -192,6 +243,13 @@ func (x *ApiClientACL) GetPrepareResults() bool { return false } +func (x *ApiClientACL) GetDeleteResults() bool { + if x != nil { + return x.DeleteResults + } + return false +} + func (x *ApiClientACL) GetDatastoreAccess() bool { if x != nil { return x.DatastoreAccess @@ -268,65 +326,79 @@ var File_acl_proto protoreflect.FileDescriptor var file_acl_proto_rawDesc = []byte{ 0x0a, 0x09, 0x61, 0x63, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x6d, 0x61, 0x6e, 0x74, - 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8e, 0x06, 0x0a, 0x0c, 0x41, 0x70, 0x69, - 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x43, 0x4c, 0x12, 0x4b, 0x0a, 0x09, 0x61, 0x6c, 0x6c, - 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x2e, 0xe2, 0xfc, - 0xe3, 0xc4, 0x01, 0x28, 0x12, 0x26, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x73, 0x20, 0x61, - 0x72, 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x20, 0x6c, - 0x65, 0x76, 0x65, 0x6c, 0x20, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x2e, 0x52, 0x08, 0x61, 0x6c, - 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x6e, 0x79, 0x5f, 0x71, 0x75, - 0x65, 0x72, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x6e, 0x79, 0x51, 0x75, - 0x65, 0x72, 0x79, 0x12, 0x58, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x71, - 0x75, 0x65, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, 0x31, 0xe2, 0xfc, 0xe3, - 0xc4, 0x01, 0x2b, 0x12, 0x29, 0x4c, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x71, 0x75, 0x65, - 0x75, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x63, - 0x61, 0x6e, 0x20, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x20, 0x74, 0x6f, 0x2e, 0x52, 0x0d, - 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x51, 0x75, 0x65, 0x75, 0x65, 0x73, 0x12, 0x21, 0x0a, - 0x0c, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, - 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x63, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, - 0x77, 0x72, 0x69, 0x74, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x61, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x72, 0x12, 0x34, 0x0a, 0x16, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, - 0x77, 0x72, 0x69, 0x74, 0x65, 0x72, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x57, 0x72, 0x69, 0x74, - 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x65, 0x63, 0x76, 0x65, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x65, 0x63, 0x76, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x6e, 0x6f, - 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x5f, 0x65, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x45, 0x64, 0x69, - 0x74, 0x6f, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x61, 0x64, - 0x6d, 0x69, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, - 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x61, 0x64, 0x12, - 0x29, 0x0a, 0x10, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x77, 0x72, - 0x69, 0x74, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x66, 0x69, 0x6c, 0x65, 0x73, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61, + 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf5, 0x07, 0x0a, 0x0c, 0x41, 0x70, 0x69, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x43, 0x4c, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x75, 0x70, + 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x18, 0x15, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, + 0x75, 0x70, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12, 0x4b, 0x0a, 0x09, 0x61, 0x6c, 0x6c, 0x5f, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x2e, 0xe2, 0xfc, 0xe3, + 0xc4, 0x01, 0x28, 0x12, 0x26, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x73, 0x20, 0x61, 0x72, + 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x20, 0x6c, 0x65, + 0x76, 0x65, 0x6c, 0x20, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x2e, 0x52, 0x08, 0x61, 0x6c, 0x6c, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x6e, 0x79, 0x5f, 0x71, 0x75, 0x65, + 0x72, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x6e, 0x79, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x12, 0x58, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x71, 0x75, + 0x65, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, 0x31, 0xe2, 0xfc, 0xe3, 0xc4, + 0x01, 0x2b, 0x12, 0x29, 0x4c, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x71, 0x75, 0x65, 0x75, + 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x63, 0x61, + 0x6e, 0x20, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x20, 0x74, 0x6f, 0x2e, 0x52, 0x0d, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x51, 0x75, 0x65, 0x75, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, + 0x72, 0x65, 0x61, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, + 0x23, 0x0a, 0x0d, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x5f, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x63, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x63, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x5f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x18, 0x18, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x42, 0x61, 0x73, 0x69, 0x63, + 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x75, 0x6e, 0x74, 0x18, 0x16, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x12, + 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x5f, 0x77, 0x72, 0x69, 0x74, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x72, 0x12, + 0x34, 0x0a, 0x16, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x5f, 0x77, 0x72, 0x69, 0x74, 0x65, 0x72, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x14, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x57, + 0x72, 0x69, 0x74, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x65, 0x63, 0x76, 0x65, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x65, 0x63, 0x76, 0x65, 0x12, 0x27, 0x0a, + 0x0f, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x5f, 0x65, 0x64, 0x69, 0x74, 0x6f, 0x72, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, + 0x45, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x72, 0x67, 0x5f, 0x61, 0x64, + 0x6d, 0x69, 0x6e, 0x18, 0x13, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6f, 0x72, 0x67, 0x41, 0x64, + 0x6d, 0x69, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x6d, 0x70, 0x65, + 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x12, 0x27, 0x0a, 0x0f, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x18, + 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, + 0x6d, 0x52, 0x65, 0x61, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x5f, 0x77, 0x72, 0x69, 0x74, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x57, 0x72, 0x69, 0x74, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x19, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x70, 0x72, 0x65, 0x70, 0x61, 0x72, - 0x65, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x61, 0x74, 0x61, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x22, 0x51, 0x0a, 0x04, 0x52, 0x6f, 0x6c, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x43, 0x4c, 0x52, - 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x32, 0x5a, 0x30, - 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, - 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x63, 0x6c, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x17, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, + 0x29, 0x0a, 0x10, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x6f, + 0x6c, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, + 0x22, 0x51, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x0b, + 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x41, 0x43, 0x4c, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x42, 0x32, 0x5a, 0x30, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, + 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, + 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x63, 0x6c, + 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/acls/proto/acl.proto b/acls/proto/acl.proto index 823b7ce79..95bb7cc8a 100644 --- a/acls/proto/acl.proto +++ b/acls/proto/acl.proto @@ -8,6 +8,9 @@ option go_package = "www.velocidex.com/golang/velociraptor/acls/proto"; message ApiClientACL { + // if this is set we allow everything - normally only set by + // server-server comms. + bool super_user = 21; bool all_query = 1 [(sem_type) = { description: "Provides arbitrary query level access.", @@ -23,22 +26,33 @@ message ApiClientACL { bool label_clients = 11; bool collect_client = 4; + bool collect_basic = 24; + bool start_hunt = 22; bool collect_server = 5; bool artifact_writer = 6; bool server_artifact_writer = 15; bool execve = 7; bool notebook_editor = 8; + + // Has the ability to add/remove/list orgs. A user with + // server_admin on the root org will also receive org_admin. + bool org_admin = 19; + + // Allows a user to run queries as another user. + bool impersonation = 20; + bool server_admin = 12; bool filesystem_read = 13; bool filesystem_write = 14; + bool network = 25; bool machine_state = 16; bool prepare_results = 17; + bool delete_results = 23; bool datastore_access = 18; // A list of roles in lieu of the permissions above. These will be // interpolated into this ACL object. repeated string roles = 9; - } // A role is a named sets of ACL permissions. A user may possess @@ -47,4 +61,4 @@ message Role { string name = 1; ApiClientACL permissions = 2; -} \ No newline at end of file +} diff --git a/acls/roles.go b/acls/roles.go index 8aac55488..66c020c0b 100644 --- a/acls/roles.go +++ b/acls/roles.go @@ -2,18 +2,175 @@ package acls import ( "errors" + "strings" acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/utils" +) + +var ( + ALL_ROLES = []string{"org_admin", "administrator", "reader", + "analyst", "investigator", + "artifact_writer", "api"} + + ALL_PERMISSIONS = []string{ + "ANY_QUERY", + "READ_RESULTS", + "LABEL_CLIENT", + "COLLECT_CLIENT", + "COLLECT_BASIC", + "START_HUNT", + "COLLECT_SERVER", + "ARTIFACT_WRITER", + "SERVER_ARTIFACT_WRITER", + "EXECVE", + "NOTEBOOK_EDITOR", + "SERVER_ADMIN", + "ORG_ADMIN", + "IMPERSONATION", + "FILESYSTEM_READ", + "FILESYSTEM_WRITE", + "NETWORK", + "MACHINE_STATE", + "PREPARE_RESULTS", + "DELETE_RESULTS", + "DATASTORE_ACCESS", + } ) func ValidateRole(role string) bool { - switch role { - case "administrator", "reader", "analyst", "investigator", "artifact_writer", "api": - return true + return utils.InString(ALL_ROLES, role) +} + +func DescribePermissions(token *acl_proto.ApiClientACL) []string { + result := []string{} + if token.AnyQuery { + result = append(result, "ANY_QUERY") + } + if token.ReadResults { + result = append(result, "READ_RESULTS") + } + if token.LabelClients { + result = append(result, "LABEL_CLIENT") + } + if token.CollectClient { + result = append(result, "COLLECT_CLIENT") + } + if token.CollectBasic { + result = append(result, "COLLECT_BASIC") + } + if token.StartHunt { + result = append(result, "START_HUNT") + } + if token.CollectServer { + result = append(result, "COLLECT_SERVER") + } + if token.ArtifactWriter { + result = append(result, "ARTIFACT_WRITER") + } + if token.ServerArtifactWriter { + result = append(result, "SERVER_ARTIFACT_WRITER") + } + if token.Execve { + result = append(result, "EXECVE") + } + if token.NotebookEditor { + result = append(result, "NOTEBOOK_EDITOR") + } + if token.ServerAdmin { + result = append(result, "SERVER_ADMIN") + } + if token.OrgAdmin { + result = append(result, "ORG_ADMIN") + } + if token.Impersonation { + result = append(result, "IMPERSONATION") + } + if token.FilesystemRead { + result = append(result, "FILESYSTEM_READ") + } + + if token.FilesystemWrite { + result = append(result, "FILESYSTEM_WRITE") + } + + if token.Network { + result = append(result, "NETWORK") + } + + if token.MachineState { + result = append(result, "MACHINE_STATE") + } + + if token.PrepareResults { + result = append(result, "PREPARE_RESULTS") + } + + if token.DeleteResults { + result = append(result, "DELETE_RESULTS") + } + + if token.DatastoreAccess { + result = append(result, "DATASTORE_ACCESS") } - return false + return result +} + +func SetTokenPermission( + token *acl_proto.ApiClientACL, permissions ...string) error { + for _, perm := range permissions { + switch strings.ToUpper(perm) { + case "ANY_QUERY": + token.AnyQuery = true + case "READ_RESULTS": + token.ReadResults = true + case "LABEL_CLIENT": + token.LabelClients = true + case "COLLECT_CLIENT": + token.CollectClient = true + case "COLLECT_BASIC": + token.CollectBasic = true + case "START_HUNT": + token.StartHunt = true + case "COLLECT_SERVER": + token.CollectServer = true + case "ARTIFACT_WRITER": + token.ArtifactWriter = true + case "SERVER_ARTIFACT_WRITER": + token.ServerArtifactWriter = true + case "EXECVE": + token.Execve = true + case "NOTEBOOK_EDITOR": + token.NotebookEditor = true + case "SERVER_ADMIN": + token.ServerAdmin = true + case "ORG_ADMIN": + token.OrgAdmin = true + case "IMPERSONATION": + token.Impersonation = true + case "FILESYSTEM_READ": + token.FilesystemRead = true + case "FILESYSTEM_WRITE": + token.FilesystemWrite = true + case "NETWORK": + token.Network = true + case "MACHINE_STATE": + token.MachineState = true + case "PREPARE_RESULTS": + token.PrepareResults = true + case "DELETE_RESULTS": + token.DeleteResults = true + case "DATASTORE_ACCESS": + token.DatastoreAccess = true + + default: + return errors.New("Unknown permission") + } + } + + return nil } func GetRolePermissions( @@ -23,13 +180,18 @@ func GetRolePermissions( for _, role := range roles { switch role { + case "org_admin": + result.OrgAdmin = true + // Admins get all query access case "administrator": - result.AllQuery = true result.AnyQuery = true result.ReadResults = true + result.Impersonation = true result.LabelClients = true result.CollectClient = true + result.CollectBasic = true + result.StartHunt = true result.CollectServer = true result.ArtifactWriter = true result.ServerArtifactWriter = true @@ -38,8 +200,16 @@ func GetRolePermissions( result.ServerAdmin = true result.FilesystemRead = true result.FilesystemWrite = true + result.Network = true result.MachineState = true result.PrepareResults = true + result.DeleteResults = true + + // An administrator for the root org is allowed to + // manipulate orgs. + if config_obj != nil && utils.IsRootOrg(config_obj.OrgId) { + result.OrgAdmin = true + } // Readers can view results but not edit or // modify anything. @@ -69,6 +239,7 @@ func GetRolePermissions( result.ReadResults = true result.NotebookEditor = true result.CollectClient = true + result.StartHunt = true result.LabelClients = true result.AnyQuery = true result.PrepareResults = true diff --git a/acls/utils.go b/acls/utils.go index 07104eed6..66855c7d9 100644 --- a/acls/utils.go +++ b/acls/utils.go @@ -1,26 +1,57 @@ package acls import ( - "github.com/pkg/errors" + "reflect" + "sort" acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" - config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/utils" ) -func GrantRoles( - config_obj *config_proto.Config, - principal string, - roles []string) error { - new_policy := &acl_proto.ApiClientACL{} +func ACLEqual(a, b *acl_proto.ApiClientACL) bool { + sort.Strings(a.Roles) + sort.Strings(b.Roles) - for _, role := range roles { - if !utils.InString(new_policy.Roles, role) { - if !ValidateRole(role) { - return errors.Errorf("Invalid role %v", role) + return utils.StringSliceEq(a.Roles, b.Roles) && a == b +} + +func CopyACL(old *acl_proto.ApiClientACL) *acl_proto.ApiClientACL { + res := *old + res.Roles = utils.CopySlice(old.Roles) + return &res +} + +// Merge the new ACL into the old +func MergeACL(old, new *acl_proto.ApiClientACL) *acl_proto.ApiClientACL { + old = CopyACL(old) + + old.Roles = utils.DeduplicateStringSlice(append(old.Roles, new.Roles...)) + + // Now set the individual ACLs + old_value := reflect.Indirect(reflect.ValueOf(old)) + new_value := reflect.Indirect(reflect.ValueOf(new)) + + res_type := old_value.Type() + for i := 0; i < res_type.NumField(); i++ { + field := res_type.Field(i) + if !field.IsExported() { + continue + } + + if field.Type.Kind() != reflect.Bool { + continue + } + + old_value := old_value.FieldByName(field.Name) + old_bool := old_value.Interface().(bool) + if !old_bool { + new_value := new_value.FieldByName(field.Name) + new_bool := new_value.Interface().(bool) + if new_bool { + old_value.SetBool(true) } - new_policy.Roles = append(new_policy.Roles, role) } } - return SetPolicy(config_obj, principal, new_policy) + + return old } diff --git a/acls/utils_test.go b/acls/utils_test.go new file mode 100644 index 000000000..71cd984f8 --- /dev/null +++ b/acls/utils_test.go @@ -0,0 +1,28 @@ +package acls + +import ( + "testing" + + "github.com/Velocidex/ordereddict" + acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" +) + +func TestMergeACL(t *testing.T) { + a := &acl_proto.ApiClientACL{ + Roles: []string{"reader"}, + CollectServer: true, + } + + b := &acl_proto.ApiClientACL{ + Roles: []string{"org_admin"}, + Execve: true, + } + + golden := ordereddict.NewDict() + golden.Set("Merge", MergeACL(a, b)) + + goldie.Assert(t, "TestMergeACL", json.MustMarshalIndent(golden)) + +} diff --git a/actions/client_info.go b/actions/client_info.go new file mode 100644 index 000000000..4fe7c3323 --- /dev/null +++ b/actions/client_info.go @@ -0,0 +1,54 @@ +package actions + +import ( + "context" + + "github.com/Showmax/go-fqdn" + actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/vql/psutils" +) + +// Return essential information about the client used for indexing +// etc. This augments the interrogation workflow via the +// Server.Internal.ClientInfo artifact. We send this message to the +// server periodically to avoid having to issue Generic.Client.Info +// hunts all the time. +func GetClientInfo( + ctx context.Context, + config_obj *config_proto.Config) *actions_proto.ClientInfo { + result := &actions_proto.ClientInfo{} + + if config_obj.Version != nil { + result.ClientName = config_obj.Version.Name + result.ClientVersion = config_obj.Version.Version + result.BuildUrl = config_obj.Version.CiBuildUrl + result.BuildTime = config_obj.Version.BuildTime + result.InstallTime = config_obj.Version.InstallTime + } + + for _, remapping := range config_obj.Remappings { + if remapping.Type == "impersonation" { + result.Hostname = remapping.Hostname + result.Fqdn = remapping.Hostname + result.System = remapping.Os + return result + } + } + + info, err := psutils.InfoWithContext(ctx) + if err == nil { + result.Hostname = info.Hostname + result.System = info.OS + result.Release = info.Platform + info.PlatformVersion + result.Architecture = utils.GetArch() + result.Fqdn = fqdn.Get() + } + + if config_obj.Client != nil { + result.Labels = config_obj.Client.Labels + } + + return result +} diff --git a/actions/events.go b/actions/events.go index 2834144c8..b096f29cf 100644 --- a/actions/events.go +++ b/actions/events.go @@ -1,6 +1,6 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -19,7 +19,7 @@ // Client Events are long lived VQL queries which stream their results // to the event handler on the server. Clients maintain a global event // table internally containing a set of event queries. The client's -// table is kept in sync with the server by compaing the table's +// table is kept in sync with the server by comparing the table's // version on each packet sent. If the server's event table is higher // than the client's the server will refresh the client's table using // the UpdateEventTable() action. @@ -30,44 +30,47 @@ import ( "context" "fmt" "sync" + "time" "google.golang.org/protobuf/proto" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" - config "www.velocidex.com/golang/velociraptor/config" config_proto "www.velocidex.com/golang/velociraptor/config/proto" + crypto_proto "www.velocidex.com/golang/velociraptor/crypto/proto" + flows_proto "www.velocidex.com/golang/velociraptor/flows/proto" "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/responder" -) - -var ( - GlobalEventTable = &EventTable{} - mu sync.Mutex + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/services/writeback" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" ) type EventTable struct { - Events []*actions_proto.VQLCollectorArgs + mu sync.Mutex + + // Context for cancelling all inflight queries in this event + // table. + Ctx context.Context + cancel func() + wg *sync.WaitGroup + + // The event table currently running + Events []*actions_proto.VQLCollectorArgs + + // The version of this event table - we only update from the + // server if the server's event table is newer. version uint64 config_obj *config_proto.Config - // This will be closed to signal that we need to abort the - // current event queries. - Done chan bool - wg sync.WaitGroup - - // Keep track of inflight queries for shutdown. This wait - // group belongs to the client's service manager. As we issue - // queries we increment it and when queries are done we - // decrement it. The service manager will wait for this before - // exiting allowing the client to shut down in an orderly - // fashion. - service_wg *sync.WaitGroup + monitoring_manager *responder.MonitoringManager } // Determine if the current table is the same as the new set of -// queries. Returns true if the queries are the same and not change is -// needed. -func (self *EventTable) equal(events []*actions_proto.VQLCollectorArgs) bool { +// queries. Returns true if the queries are the same and no change is +// needed. NOTE: Assumes the order of queries and Env variables is +// deterministic and consistent. +func (self *EventTable) Equal(events []*actions_proto.VQLCollectorArgs) bool { if len(events) != len(self.Events) { return false } @@ -101,149 +104,173 @@ func (self *EventTable) equal(events []*actions_proto.VQLCollectorArgs) bool { // Teardown all the current quries. Blocks until they all shut down. func (self *EventTable) Close() { - logger := logging.GetLogger(self.config_obj, &logging.ClientComponent) - logger.Info("Closing EventTable\n") + self.mu.Lock() + defer self.mu.Unlock() - close(self.Done) + self.close() +} + +// Actually close the table without lock +func (self *EventTable) close() { + if self.config_obj != nil { + logger := logging.GetLogger(self.config_obj, &logging.ClientComponent) + logger.Info("Closing EventTable\n") + } + + self.cancel() + + // Wait until the queries have completed. self.wg.Wait() + + // Clear the list of events we are tracking - we are an empty + // event table right now - so further updates will restart the + // queries again. + self.Events = nil + self.version = 0 } -func GlobalEventTableVersion() uint64 { - mu.Lock() - defer mu.Unlock() +func (self *EventTable) Version() uint64 { + self.mu.Lock() + defer self.mu.Unlock() - return GlobalEventTable.version + return self.version } -func update( +func (self *EventTable) Update( + ctx context.Context, + wg *sync.WaitGroup, config_obj *config_proto.Config, - responder *responder.Responder, - table *actions_proto.VQLEventTable) (*EventTable, error, bool) { + output_chan chan *crypto_proto.VeloMessage, + table *actions_proto.VQLEventTable) (error, bool) { - mu.Lock() - defer mu.Unlock() + self.mu.Lock() + defer self.mu.Unlock() // Only update the event table if we need to. - if table.Version <= GlobalEventTable.version { - return GlobalEventTable, nil, false + if table.Version <= self.version { + return nil, false } // If the new update is identical to the old queries we wont // restart. This can happen e.g. if the server changes label - // groups and recaculates the table version but the actual + // groups and recalculates the table version but the actual // queries dont end up changing. - if GlobalEventTable.equal(table.Event) { + if self.Equal(table.Event) { logger := logging.GetLogger(config_obj, &logging.ClientComponent) logger.Info("Client event query update %v did not "+ "change queries, skipping", table.Version) // Update the version only but keep queries the same. - GlobalEventTable.version = table.Version - return GlobalEventTable, nil, false + self.version = table.Version + return nil, false } - // Close the old table. - if GlobalEventTable.Done != nil { - GlobalEventTable.Close() + // Close the old table and wait for it to finish. + self.close() + + // Reset the event table and start from scratch. + self.Events = nil + + // Make a copy of the events so we can own them. + for _, e := range table.Event { + self.Events = append(self.Events, + proto.Clone(e).(*actions_proto.VQLCollectorArgs)) } - // Reset the table. - GlobalEventTable.Events = table.Event - GlobalEventTable.version = table.Version - GlobalEventTable.Done = make(chan bool) - GlobalEventTable.config_obj = config_obj - GlobalEventTable.service_wg = &sync.WaitGroup{} + self.version = table.Version + self.wg = &sync.WaitGroup{} + self.Ctx, self.cancel = context.WithCancel(ctx) - return GlobalEventTable, nil, true /* changed */ + return nil, true /* changed */ } -func NewEventTable( - config_obj *config_proto.Config, - responder *responder.Responder, - table *actions_proto.VQLEventTable) *EventTable { - result := &EventTable{ - Events: table.Event, - version: table.Version, - Done: make(chan bool), - config_obj: config_obj, - } +// Make a copy of the event table and appand any config enforced +// additional event queries. +func (self *EventTable) GetEventQueries( + ctx context.Context, + config_obj *config_proto.Config) ([]*actions_proto.VQLCollectorArgs, error) { - return result -} + self.mu.Lock() + defer self.mu.Unlock() -type UpdateEventTable struct{} + result := make([]*actions_proto.VQLCollectorArgs, 0, len(self.Events)) + result = append(result, self.Events...) -func (self UpdateEventTable) Run( - config_obj *config_proto.Config, - ctx context.Context, - responder *responder.Responder, - arg *actions_proto.VQLEventTable) { + // If there are no built in additional event artifacts we are done + // - just run the queries from the event table. + if config_obj.Client == nil || + len(config_obj.Client.AdditionalEventArtifacts) == 0 { + return result, nil + } - // Make a new table. - table, err, changed := update(config_obj, responder, arg) + launcher, err := services.GetLauncher(config_obj) if err != nil { - responder.RaiseError(ctx, fmt.Sprintf( - "Error updating global event table: %v", err)) - return + return result, err } - // No change required, skip it. - if !changed { - // We still need to write the new version - err = update_writeback(config_obj, arg) - if err != nil { - responder.RaiseError(ctx, fmt.Sprintf( - "Unable to write events to writeback: %v", err)) - } else { - responder.Return(ctx) - } - return + // Config enforced event queries are compiled using the built in + // repository because we do no have access to the server + // repository yet! + manager, err := services.GetRepositoryManager(config_obj) + if err != nil { + return result, err + } + repository, err := manager.GetGlobalRepository(config_obj) + if err != nil { + return result, err } - logger := logging.GetLogger(config_obj, &logging.ClientComponent) + // Compile the built in artifacts + queries, err := launcher.CompileCollectorArgs(ctx, config_obj, + acl_managers.NullACLManager{}, repository, + services.CompilerOptions{}, &flows_proto.ArtifactCollectorArgs{ + Artifacts: config_obj.Client.AdditionalEventArtifacts, + }) - // Make a context for the VQL query. - new_ctx, cancel := context.WithCancel(context.Background()) + if err != nil { + return result, err + } + + result = append(result, queries...) + return result, nil +} - // Cancel the context when the cancel channel is closed. - go func() { - mu.Lock() - done := table.Done - mu.Unlock() +func (self *EventTable) StartQueries( + ctx context.Context, + config_obj *config_proto.Config, + output_chan chan *crypto_proto.VeloMessage) { - <-done - logger.Info("UpdateEventTable: Closing all contexts") - cancel() - }() + logger := logging.GetLogger(config_obj, &logging.ClientComponent) + + events, err := self.GetEventQueries(ctx, config_obj) + if err != nil { + logger := logging.GetLogger(config_obj, &logging.ClientComponent) + logger.Error("While appending initial event artifacts: %v", err) + } // Start a new query for each event. - action_obj := &VQLClientAction{} - table.wg.Add(len(table.Events)) - table.service_wg.Add(len(table.Events)) + for _, event := range events { - for _, event := range table.Events { - query_responder := responder.Copy() + // Name of the query we are running. There must be at least + // one query with a name. + artifact_name := utils.GetQueryName(event.Query) + if artifact_name == "" { + continue + } - go func(event *actions_proto.VQLCollectorArgs) { - defer table.wg.Done() - defer table.service_wg.Done() - - // Name of the query we are running. - name := "" - for _, q := range event.Query { - if q.Name != "" { - name = q.Name - } - } + logger.Info("Starting monitoring query %s", artifact_name) + query_responder := responder.NewMonitoringResponder( + ctx, config_obj, self.monitoring_manager, + output_chan, artifact_name, event.QueryId) - if name != "" { - logger.Info("Starting monitoring query %s", name) - } - query_responder.Artifact = name + self.wg.Add(1) + go func(event *actions_proto.VQLCollectorArgs) { + defer self.wg.Done() + defer query_responder.Close() - // Event tables never time out + // Event tables get refreshed by default every 12 hours. if event.Timeout == 0 { - event.Timeout = 99999999 + event.Timeout = 12 * 60 * 60 } // Dont heartbeat too often for event queries @@ -252,52 +279,151 @@ func (self UpdateEventTable) Run( event.Heartbeat = 300 // 5 minutes } - action_obj.StartQuery( - config_obj, new_ctx, query_responder, event) - if name != "" { - logger.Info("Finished monitoring query %s", name) + // Start the query - if it is an event query this will + // never complete until it is cancelled. + self.RunQuery(self.Ctx, config_obj, + artifact_name, query_responder, event) + if artifact_name != "" { + logger.Info("Finished monitoring query %s", artifact_name) } }(proto.Clone(event).(*actions_proto.VQLCollectorArgs)) } +} - err = update_writeback(config_obj, arg) - if err != nil { - responder.RaiseError(ctx, fmt.Sprintf( - "Unable to write events to writeback: %v", err)) - return - } +func (self *EventTable) RunQuery( + ctx context.Context, + config_obj *config_proto.Config, + artifact_name string, + query_responder responder.Responder, + event *actions_proto.VQLCollectorArgs) { + + wg := &sync.WaitGroup{} + defer wg.Wait() + + refresh_timeout := event.Timeout + event.Timeout = 999999 + + for { + sub_ctx, cancel := context.WithCancel(ctx) + + refresh := utils.Jitter(time.Second * time.Duration(refresh_timeout)) + + // Start the query - if it is an event query this will not + // complete until we cancell it due to refresh. If it is not + // an event query, it will complete sooner but we wont start + // it again until the refresh time. + wg.Add(1) + go func() { + defer wg.Done() - responder.Return(ctx) + query_responder.Log(ctx, logging.DEBUG, + fmt.Sprintf("Starting monitoring query %s with refresh in %v", + artifact_name, refresh.Round(2).String())) + + action_obj := &VQLClientAction{} + action_obj.StartQuery( + config_obj, sub_ctx, query_responder, event) + }() + + select { + // Exit completely when the parent ctx is done. + case <-ctx.Done(): + cancel() + return + + // When the deadline fires, we refresh the query. + case <-time.After(refresh): + query_responder.Log(ctx, logging.DEBUG, + fmt.Sprintf("Refreshing monitoring query %s", artifact_name)) + cancel() + + // Wait here for it to be done. + wg.Wait() + } + } } -func update_writeback( +func (self *EventTable) StartFromWriteback( + ctx context.Context, wg *sync.WaitGroup, config_obj *config_proto.Config, - event_table *actions_proto.VQLEventTable) error { + output_chan chan *crypto_proto.VeloMessage) { - // Store the event table in the Writeback file. - config_copy := proto.Clone(config_obj).(*config_proto.Config) - event_copy := proto.Clone(event_table).(*actions_proto.VQLEventTable) - config_copy.Writeback.EventQueries = event_copy + // Get the event table from the writeback if possible. + var event_table *actions_proto.VQLEventTable - return config.UpdateWriteback(config_copy) + writeback_service := writeback.GetWritebackService() + writeback, err := writeback_service.GetWriteback(config_obj) + if err == nil && writeback.EventQueries != nil { + event_table = writeback.EventQueries + self.UpdateEventTable(ctx, wg, config_obj, output_chan, event_table) + } } -func InitializeEventTable(ctx context.Context, service_wg *sync.WaitGroup) { - mu.Lock() - GlobalEventTable = &EventTable{ - service_wg: service_wg, +func (self *EventTable) UpdateEventTable( + ctx context.Context, + wg *sync.WaitGroup, + config_obj *config_proto.Config, + output_chan chan *crypto_proto.VeloMessage, + update_table *actions_proto.VQLEventTable) { + + // Make a new table if needed. + err, changed := self.Update( + ctx, wg, config_obj, output_chan, update_table) + if err != nil { + responder.MakeErrorResponse( + output_chan, "F.Monitoring", fmt.Sprintf( + "Error updating global event table: %v", err)) + return } - mu.Unlock() - // When the context is finished, tear down the event table. - go func() { - <-ctx.Done() + writeback_service := writeback.GetWritebackService() - mu.Lock() - if GlobalEventTable.Done != nil { - close(GlobalEventTable.Done) + // No change required, skip it. + if !changed { + // We still need to write the new version + err = writeback_service.MutateWriteback(config_obj, + func(wb *config_proto.Writeback) error { + wb.EventQueries = update_table + return writeback.WritebackUpdateLevel2 + }) + if err != nil { + responder.MakeErrorResponse(output_chan, "F.Monitoring", + fmt.Sprintf("Unable to write events to writeback: %v", err)) } - mu.Unlock() - }() + return + } + + // Kick off the queries + self.StartQueries(ctx, config_obj, output_chan) + + // Update the writeback + err = writeback_service.MutateWriteback(config_obj, + func(wb *config_proto.Writeback) error { + wb.EventQueries = update_table + return writeback.WritebackUpdateLevel2 + }) + if err != nil { + responder.MakeErrorResponse(output_chan, "F.Monitoring", + fmt.Sprintf("Unable to write events to writeback: %v", err)) + return + } +} + +func NewEventTable( + ctx context.Context, + wg *sync.WaitGroup, + config_obj *config_proto.Config) *EventTable { + + sub_ctx, cancel := context.WithCancel(ctx) + self := &EventTable{ + Ctx: sub_ctx, + cancel: cancel, + + // Used to wait for close() + wg: &sync.WaitGroup{}, + config_obj: config_obj, + monitoring_manager: responder.NewMonitoringManager(ctx), + } + return self } diff --git a/actions/events_test.go b/actions/events_test.go index 2ebea2255..bcbce4faf 100644 --- a/actions/events_test.go +++ b/actions/events_test.go @@ -2,25 +2,28 @@ package actions_test import ( "context" + "fmt" "io/ioutil" "os" "sync" "testing" "time" - "github.com/alecthomas/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "www.velocidex.com/golang/velociraptor/actions" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" + crypto_proto "www.velocidex.com/golang/velociraptor/crypto/proto" + "www.velocidex.com/golang/velociraptor/datastore" "www.velocidex.com/golang/velociraptor/file_store/test_utils" flows_proto "www.velocidex.com/golang/velociraptor/flows/proto" "www.velocidex.com/golang/velociraptor/responder" "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/services/client_monitoring" - "www.velocidex.com/golang/velociraptor/services/labels" + "www.velocidex.com/golang/velociraptor/services/writeback" "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/utils/tempfile" "www.velocidex.com/golang/velociraptor/vtesting" + "www.velocidex.com/golang/velociraptor/vtesting/assert" _ "www.velocidex.com/golang/velociraptor/result_sets/timed" ) @@ -29,6 +32,8 @@ var ( artifact_definitions = []string{` name: EventArtifact1 type: CLIENT_EVENT +parameters: +- name: Foo sources: - query: SELECT * FROM info() `, ` @@ -42,139 +47,177 @@ sources: type EventsTestSuite struct { test_utils.TestSuite client_id string - responder *responder.Responder + responder *responder.TestResponderType writeback string - Clock utils.Clock + event_table *actions.EventTable + + closer func() } func (self *EventsTestSuite) SetupTest() { - self.TestSuite.SetupTest() - - assert.NoError( - self.T(), self.Sm.Start(client_monitoring.StartClientMonitoringService)) - - self.client_id = "C.2232" - self.Clock = &utils.IncClock{} + self.ConfigObj = self.LoadConfig() + self.LoadArtifactsIntoConfig(artifact_definitions) - tmpfile, err := ioutil.TempFile("", "") + // Set a tempfile for the writeback we need to check that the + // new event query is written there. + tmpfile, err := tempfile.TempFile("") assert.NoError(self.T(), err) tmpfile.Close() - // Set a tempfile for the writeback we need to check that the - // new event query is written there. self.writeback = tmpfile.Name() self.ConfigObj.Client.WritebackLinux = self.writeback self.ConfigObj.Client.WritebackWindows = self.writeback self.ConfigObj.Client.WritebackDarwin = self.writeback + self.ConfigObj.Services.ClientMonitoring = true + self.ConfigObj.Services.IndexServer = true - self.responder = responder.TestResponder() + datastore.SetGlobalDatastore(context.Background(), + self.ConfigObj.Datastore.Implementation, self.ConfigObj) - actions.GlobalEventTable = actions.NewEventTable( - self.ConfigObj, self.responder, + self.TestSuite.SetupTest() + + writeback_service := writeback.GetWritebackService() + writeback_service.LoadWriteback(self.ConfigObj) + + self.client_id = "C.2232" + self.closer = utils.MockTime(&utils.IncClock{}) + + client_info_manager, err := services.GetClientInfoManager(self.ConfigObj) + assert.NoError(self.T(), err) + + client_info_manager.Set(self.Ctx, &services.ClientInfo{ + ClientInfo: &actions_proto.ClientInfo{ + ClientId: self.client_id, + }, + }) + + self.responder = responder.TestResponderWithFlowId( + self.ConfigObj, "EventsTestSuite") + self.event_table = actions.NewEventTable( + self.Ctx, self.Wg, self.ConfigObj) + self.event_table.UpdateEventTable( + self.Ctx, self.Wg, self.ConfigObj, + self.responder.Output(), &actions_proto.VQLEventTable{}) +} - self.LoadArtifacts(artifact_definitions) +func (self *EventsTestSuite) InitializeEventTable(ctx context.Context, + wg *sync.WaitGroup, output_chan chan *crypto_proto.VeloMessage) *actions.EventTable { + result := actions.NewEventTable(ctx, wg, self.ConfigObj) + result.UpdateEventTable(ctx, wg, self.ConfigObj, + output_chan, &actions_proto.VQLEventTable{}) + + return result } func (self *EventsTestSuite) TearDownTest() { self.TestSuite.TearDownTest() + if self.closer != nil { + self.closer() + } + os.Remove(self.writeback) // clean up file buffer } -var server_state = &flows_proto.ClientEventTable{ - Artifacts: &flows_proto.ArtifactCollectorArgs{ - // These apply to all labels. - Artifacts: []string{"EventArtifact1"}, - }, - - // If the client is labeled as "Label1" then it will - // receive these - LabelEvents: []*flows_proto.LabelEvents{{ - Label: "Label1", +func server_state() *flows_proto.ClientEventTable { + return &flows_proto.ClientEventTable{ Artifacts: &flows_proto.ArtifactCollectorArgs{ - Artifacts: []string{"EventArtifact2"}, - }}, - }, + // These apply to all labels. + Artifacts: []string{"EventArtifact1"}, + }, + + // If the client is labeled as "Label1" then it will + // receive these + LabelEvents: []*flows_proto.LabelEvents{{ + Label: "Label1", + Artifacts: &flows_proto.ArtifactCollectorArgs{ + Artifacts: []string{"EventArtifact2"}, + }}, + }, + } } func (self *EventsTestSuite) TestEventTableUpdate() { - client_manager := services.ClientEventManager() - client_manager.(*client_monitoring.ClientEventTable).Clock = self.Clock + client_manager, err := services.ClientEventManager(self.ConfigObj) + assert.NoError(self.T(), err) + + wg := &sync.WaitGroup{} + defer wg.Wait() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) + ctx, cancel := context.WithTimeout(self.Ctx, time.Second*60) defer cancel() // Wait until the entire event table is cleaned up. - wg := &sync.WaitGroup{} - actions.InitializeEventTable(ctx, wg) - defer wg.Wait() + output_chan, _ := responder.NewMessageDrain(ctx) + table := self.InitializeEventTable(ctx, wg, output_chan) require.NoError(self.T(), client_manager.SetClientMonitoringState( - ctx, self.ConfigObj, "", server_state)) + ctx, self.ConfigObj, "", server_state())) // Check the version of the initial Event table it should be 0 - version := actions.GlobalEventTableVersion() + version := table.Version() assert.Equal(self.T(), uint64(0), version) // We definitely need to update the table on this client. assert.True(self.T(), client_manager.CheckClientEventsVersion( + self.Ctx, self.ConfigObj, self.client_id, version)) // Get the new table message := client_manager.GetClientUpdateEventTableMessage( - self.ConfigObj, self.client_id) + self.Ctx, self.ConfigObj, self.client_id) // Only one query will be selected now since no label is set // on the client. assert.Equal(self.T(), len(message.UpdateEventTable.Event), 1) - assert.Equal(self.T(), getQueryName(message.UpdateEventTable.Event[0]), - "EventArtifact1") + assert.Equal(self.T(), utils.GetQueryName( + message.UpdateEventTable.Event[0].Query), "EventArtifact1") // Set the new table, this will execute the new queries and // start the new table. actions.QueryLog.Clear() - actions.UpdateEventTable{}.Run(self.ConfigObj, ctx, self.responder, + table.UpdateEventTable(ctx, wg, self.ConfigObj, output_chan, message.UpdateEventTable) // Table version was upgraded - version = actions.GlobalEventTableVersion() + version = table.Version() assert.NotEqual(self.T(), version, 0) // And we ran some queries. vtesting.WaitUntil(5*time.Second, self.T(), func() bool { - return len(actions.QueryLog.Get()) > 0 + return len(actions.QueryLog.Get()) > 1 }) + actions.QueryLog.Clear() // We no longer need to update the event table - it is up to date. assert.False(self.T(), client_manager.CheckClientEventsVersion( - self.ConfigObj, self.client_id, - actions.GlobalEventTableVersion())) + self.Ctx, self.ConfigObj, self.client_id, + table.Version())) // Now we set a label on the client. This should cause the // event table to be recalculated but since the label does not // actually change the label groups, the new event table will // be the same as the old one, except the version will be // advanced. - label_manager := services.GetLabeler() - label_manager.(*labels.Labeler).Clock = self.Clock + label_manager := services.GetLabeler(self.ConfigObj) require.NoError(self.T(), - label_manager.SetClientLabel(self.ConfigObj, self.client_id, - "Foobar")) + label_manager.SetClientLabel( + self.Ctx, self.ConfigObj, self.client_id, "Foobar")) // Setting the label will cause the client_monitoring manager // to want to upgrade the event table. assert.True(self.T(), client_manager.CheckClientEventsVersion( - self.ConfigObj, self.client_id, - actions.GlobalEventTableVersion())) + self.Ctx, self.ConfigObj, self.client_id, + table.Version())) new_message := client_manager.GetClientUpdateEventTableMessage( - self.ConfigObj, self.client_id) + self.Ctx, self.ConfigObj, self.client_id) assert.True(self.T(), new_message.UpdateEventTable.Version > message.UpdateEventTable.Version) @@ -182,41 +225,48 @@ func (self *EventsTestSuite) TestEventTableUpdate() { // The new table has 1 queries still since it has not really changed. assert.Equal(self.T(), len(new_message.UpdateEventTable.Event), 1) - // Lets update the event table with the new version. + // Now check that no updates are performed: We clear the query log + // and send an update. No new queries should be running. actions.QueryLog.Clear() - actions.UpdateEventTable{}.Run(self.ConfigObj, ctx, self.responder, + + table.UpdateEventTable(ctx, wg, self.ConfigObj, output_chan, new_message.UpdateEventTable) // Wait for the event table version to change vtesting.WaitUntil(5*time.Second, self.T(), func() bool { - return version != actions.GlobalEventTableVersion() + return version != table.Version() }) // But the tables have not really changed, so the query will // not be updated. - assert.Equal(self.T(), len(actions.QueryLog.Get()), 0) + queries := actions.QueryLog.Get() + if len(queries) != 0 { + fmt.Printf("Queries that ran %v\n", queries) + } + assert.Equal(self.T(), len(queries), 0) // Now lets set the label to Label1 require.NoError(self.T(), - label_manager.SetClientLabel(self.ConfigObj, self.client_id, - "Label1")) + label_manager.SetClientLabel( + self.Ctx, self.ConfigObj, + self.client_id, "Label1")) // We need to update the table again (takes a while for the // client manager to notice the label change). vtesting.WaitUntil(5*time.Second, self.T(), func() bool { return client_manager.CheckClientEventsVersion( - self.ConfigObj, self.client_id, - actions.GlobalEventTableVersion()) + self.Ctx, self.ConfigObj, self.client_id, + table.Version()) }) new_message = client_manager.GetClientUpdateEventTableMessage( - self.ConfigObj, self.client_id) + self.Ctx, self.ConfigObj, self.client_id) // The new table has 2 event queries - one for the All label // and one for Label1 label. assert.Equal(self.T(), len(new_message.UpdateEventTable.Event), 2) - actions.UpdateEventTable{}.Run(self.ConfigObj, ctx, self.responder, + table.UpdateEventTable(ctx, wg, self.ConfigObj, output_chan, new_message.UpdateEventTable) // Wait for the event table to be swapped. @@ -235,15 +285,119 @@ func (self *EventsTestSuite) TestEventTableUpdate() { // Make sure the event queries end up in the writeback file assert.Contains(self.T(), string(data), "EventArtifact1") assert.Contains(self.T(), string(data), "EventArtifact2") + + // The below checks that the event table is updated if only a + // parameter is changed. + + // Check that Foo is empty right now + assert.Equal(self.T(), "", table.Events[0].Env[0].Value) + + // Update the monitoring table but only change artifact + // parameters. Set Foo to "X" + new_state := server_state() + new_state.Artifacts.Specs = append(new_state.Artifacts.Specs, + &flows_proto.ArtifactSpec{ + Artifact: "EventArtifact1", + Parameters: &flows_proto.ArtifactParameters{ + Env: []*actions_proto.VQLEnv{ + {Key: "Foo", Value: "X"}, + }, + }, + }) + + require.NoError(self.T(), client_manager.SetClientMonitoringState( + ctx, self.ConfigObj, "", new_state)) + + new_message = client_manager.GetClientUpdateEventTableMessage( + self.Ctx, self.ConfigObj, self.client_id) + + // Force the update on the table. + table.UpdateEventTable(ctx, wg, self.ConfigObj, output_chan, + new_message.UpdateEventTable) + + // The update took hold - the new parameter value is "X" + assert.Equal(self.T(), "X", table.Events[0].Env[0].Value) } -func getQueryName(args *actions_proto.VQLCollectorArgs) string { - for _, query := range args.Query { - if query.Name != "" { - return query.Name - } - } - return "" +// What do we consider a change in the event table. The server may +// send updated event tables frequently but we do not want to +// interrupt the event tables if the queries do not really +// change. This checks we skip the table update if it is the same as +// before. +func (self *EventsTestSuite) TestEventEqual() { + client_manager, err := services.ClientEventManager(self.ConfigObj) + assert.NoError(self.T(), err) + + ctx, cancel := context.WithTimeout(self.Ctx, time.Second*60) + defer cancel() + + // Wait until the entire event table is cleaned up. + wg := &sync.WaitGroup{} + output_chan, _ := responder.NewMessageDrain(ctx) + table := self.InitializeEventTable(ctx, wg, output_chan) + _ = table + + require.NoError(self.T(), client_manager.SetClientMonitoringState( + ctx, self.ConfigObj, "", server_state())) + + message := client_manager.GetClientUpdateEventTableMessage( + self.Ctx, self.ConfigObj, self.client_id) + + // Update the table for the base message. + err, ok := table.Update(ctx, wg, self.ConfigObj, output_chan, + message.UpdateEventTable) + assert.NoError(self.T(), err) + assert.True(self.T(), ok) + + // Now we try check if the table will update under certain conditions. + + // Increase the version but no difference in content at all + message.UpdateEventTable.Version += 100 + err, ok = table.Update(ctx, wg, self.ConfigObj, output_chan, + message.UpdateEventTable) + assert.NoError(self.T(), err) + assert.False(self.T(), ok) + + // A query was added to the table + message.UpdateEventTable.Version += 100 + message.UpdateEventTable.Event[0].Query = append( + message.UpdateEventTable.Event[0].Query, + &actions_proto.VQLRequest{ + VQL: "SELECT * FROM info()", + }) + + err, ok = table.Update(ctx, wg, self.ConfigObj, output_chan, + message.UpdateEventTable) + assert.NoError(self.T(), err) + + // Yes this is a new query! + assert.True(self.T(), ok) + + // Now add a new parameter - this is also an update + message.UpdateEventTable.Version += 100 + message.UpdateEventTable.Event[0].Env = append( + message.UpdateEventTable.Event[0].Env, &actions_proto.VQLEnv{ + Key: "Foo", + Value: "Bar", + }) + + err, ok = table.Update(ctx, wg, self.ConfigObj, output_chan, + message.UpdateEventTable) + assert.NoError(self.T(), err) + + // Yes this is a new query! + assert.True(self.T(), ok) + + // Change the parameter + message.UpdateEventTable.Version += 100 + message.UpdateEventTable.Event[0].Env[0].Value = "Baz" + + err, ok = table.Update(ctx, wg, self.ConfigObj, output_chan, + message.UpdateEventTable) + assert.NoError(self.T(), err) + + // Yes this is a new query! + assert.True(self.T(), ok) } func TestEventsTestSuite(t *testing.T) { diff --git a/actions/foreman.go b/actions/foreman.go deleted file mode 100644 index a8bc53fab..000000000 --- a/actions/foreman.go +++ /dev/null @@ -1,46 +0,0 @@ -/* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -package actions - -import ( - "context" - - actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" - config "www.velocidex.com/golang/velociraptor/config" - config_proto "www.velocidex.com/golang/velociraptor/config/proto" - "www.velocidex.com/golang/velociraptor/responder" -) - -type UpdateForeman struct{} - -func (self UpdateForeman) Run( - config_obj *config_proto.Config, - ctx context.Context, - responder *responder.Responder, - arg *actions_proto.ForemanCheckin) { - - if arg.LastHuntTimestamp > config_obj.Writeback.HuntLastTimestamp { - config_obj.Writeback.HuntLastTimestamp = arg.LastHuntTimestamp - err := config.UpdateWriteback(config_obj) - if err != nil { - responder.RaiseError(ctx, err.Error()) - return - } - } - responder.Return(ctx) -} diff --git a/actions/progress.go b/actions/progress.go new file mode 100644 index 000000000..f2f050aaf --- /dev/null +++ b/actions/progress.go @@ -0,0 +1,94 @@ +package actions + +import ( + "bytes" + "context" + "runtime/pprof" + "sync" + "time" + + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/types" +) + +type ProgressThrottler struct { + mu sync.Mutex + delegate types.Throttler + progress_timeout time.Duration + heartbeat time.Time + + // Will be called when an alarm is fired. + cancel func() +} + +func (self *ProgressThrottler) ChargeOp() { + self.mu.Lock() + self.heartbeat = utils.Now() + self.mu.Unlock() + self.delegate.ChargeOp() +} + +func (self *ProgressThrottler) Close() { + self.delegate.Close() +} + +func (self *ProgressThrottler) Start( + ctx context.Context, scope vfilter.Scope) { + for { + select { + case <-ctx.Done(): + return + + case <-time.After(self.progress_timeout): + self.mu.Lock() + now := utils.Now() + if self.progress_timeout.Nanoseconds() > 0 && + now.After(self.heartbeat.Add(self.progress_timeout)) { + self.mu.Unlock() + self.exitWithError(scope) + return + } + self.mu.Unlock() + } + } +} + +func (self *ProgressThrottler) exitWithError(scope vfilter.Scope) { + scope.Log("ERROR:No progress made in %v seconds! aborting.", + self.progress_timeout) + + buf := bytes.Buffer{} + p := pprof.Lookup("goroutine") + if p != nil { + _ = p.WriteTo(&buf, 1) + scope.Log("Goroutine dump: %v", buf.String()) + } + + buf = bytes.Buffer{} + p = pprof.Lookup("mutex") + if p != nil { + _ = p.WriteTo(&buf, 1) + scope.Log("Mutex dump: %v", buf.String()) + } + + for _, q := range QueryLog.Get() { + scope.Log("Recent Query: %v", q) + } + self.cancel() +} + +func NewProgressThrottler( + ctx context.Context, scope vfilter.Scope, + cancel func(), + throttler types.Throttler, + progress_timeout time.Duration) types.Throttler { + result := &ProgressThrottler{ + cancel: cancel, + delegate: throttler, + progress_timeout: progress_timeout, + } + + go result.Start(ctx, scope) + return result +} diff --git a/actions/proto/transport.pb.go b/actions/proto/transport.pb.go index 89b2518ce..d9e2976e2 100644 --- a/actions/proto/transport.pb.go +++ b/actions/proto/transport.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: transport.proto package proto @@ -26,10 +23,15 @@ type Range struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - FileOffset int64 `protobuf:"varint,1,opt,name=file_offset,json=fileOffset,proto3" json:"file_offset,omitempty"` + // Range offset in the underlying file. + FileOffset int64 `protobuf:"varint,1,opt,name=file_offset,json=fileOffset,proto3" json:"file_offset,omitempty"` + // Range offset in the underlying file OriginalOffset int64 `protobuf:"varint,2,opt,name=original_offset,json=originalOffset,proto3" json:"original_offset,omitempty"` - FileLength int64 `protobuf:"varint,3,opt,name=file_length,json=fileLength,proto3" json:"file_length,omitempty"` - Length int64 `protobuf:"varint,4,opt,name=length,proto3" json:"length,omitempty"` + // The length of data that exists in the underlying file. May be 0 + // if the range is sparse and has no underlying storage. + FileLength int64 `protobuf:"varint,3,opt,name=file_length,json=fileLength,proto3" json:"file_length,omitempty"` + // Length of this range. + Length int64 `protobuf:"varint,4,opt,name=length,proto3" json:"length,omitempty"` } func (x *Range) Reset() { @@ -146,8 +148,9 @@ type PathSpec struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - Accessor string `protobuf:"bytes,3,opt,name=accessor,proto3" json:"accessor,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Components []string `protobuf:"bytes,4,rep,name=components,proto3" json:"components,omitempty"` + Accessor string `protobuf:"bytes,3,opt,name=accessor,proto3" json:"accessor,omitempty"` } func (x *PathSpec) Reset() { @@ -189,6 +192,13 @@ func (x *PathSpec) GetPath() string { return "" } +func (x *PathSpec) GetComponents() []string { + if x != nil { + return x.Components + } + return nil +} + func (x *PathSpec) GetAccessor() string { if x != nil { return x.Accessor @@ -213,6 +223,14 @@ type FileBuffer struct { StoredSize uint64 `protobuf:"varint,8,opt,name=stored_size,json=storedSize,proto3" json:"stored_size,omitempty"` IsSparse bool `protobuf:"varint,9,opt,name=is_sparse,json=isSparse,proto3" json:"is_sparse,omitempty"` Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + // If this is > 0 then then data field contains already compressed + // data. The length of the uncompressed data will be given here. + UncompressedLength uint64 `protobuf:"varint,17,opt,name=uncompressed_length,json=uncompressedLength,proto3" json:"uncompressed_length,omitempty"` + // For uploaders that do not transfer the bulk data inside the + // data field, we need a way to specify how much data was + // transferred in thie buffer. In the usual uploader this will be + // the len(data). + DataLength uint64 `protobuf:"varint,16,opt,name=data_length,json=dataLength,proto3" json:"data_length,omitempty"` FlowId string `protobuf:"bytes,4,opt,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` Eof bool `protobuf:"varint,5,opt,name=eof,proto3" json:"eof,omitempty"` // Set when the file is sparse. @@ -221,6 +239,14 @@ type FileBuffer struct { Atime int64 `protobuf:"varint,11,opt,name=atime,proto3" json:"atime,omitempty"` Ctime int64 `protobuf:"varint,12,opt,name=ctime,proto3" json:"ctime,omitempty"` Btime int64 `protobuf:"varint,13,opt,name=btime,proto3" json:"btime,omitempty"` + // Set when the actual file is stored somewhere else (e.g. S3) + Reference string `protobuf:"bytes,14,opt,name=reference,proto3" json:"reference,omitempty"` + // An incrementing number of uploads across the entire + // collection. Velociraptor file uploads are stored per collection + // and not per query so this number is unique across all the + // queries in the collection. It amounts to the row id on the + // collections uploads result set. + UploadNumber int64 `protobuf:"varint,15,opt,name=upload_number,json=uploadNumber,proto3" json:"upload_number,omitempty"` } func (x *FileBuffer) Reset() { @@ -297,6 +323,20 @@ func (x *FileBuffer) GetData() []byte { return nil } +func (x *FileBuffer) GetUncompressedLength() uint64 { + if x != nil { + return x.UncompressedLength + } + return 0 +} + +func (x *FileBuffer) GetDataLength() uint64 { + if x != nil { + return x.DataLength + } + return 0 +} + func (x *FileBuffer) GetFlowId() string { if x != nil { return x.FlowId @@ -346,6 +386,20 @@ func (x *FileBuffer) GetBtime() int64 { return 0 } +func (x *FileBuffer) GetReference() string { + if x != nil { + return x.Reference + } + return "" +} + +func (x *FileBuffer) GetUploadNumber() int64 { + if x != nil { + return x.UploadNumber + } + return 0 +} + type ForemanCheckin struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -401,6 +455,158 @@ func (x *ForemanCheckin) GetLastEventTableVersion() uint64 { return 0 } +// An UploadTransaction represents an intention for the client to +// begin an upload. The upload will proceed in the future. The actual +// upload may be cancelled or timed out and then can be resumed by the +// user. +// +// The goal of this message is to capture state as much as possible to +// allow the upload to resume, i.e. reflect the uploader interface +// args. +type UploadTransaction struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` + Accessor string `protobuf:"bytes,2,opt,name=accessor,proto3" json:"accessor,omitempty"` + StoreAsName string `protobuf:"bytes,3,opt,name=store_as_name,json=storeAsName,proto3" json:"store_as_name,omitempty"` + Components []string `protobuf:"bytes,13,rep,name=components,proto3" json:"components,omitempty"` + ExpectedSize int64 `protobuf:"varint,4,opt,name=expected_size,json=expectedSize,proto3" json:"expected_size,omitempty"` + Mtime int64 `protobuf:"varint,5,opt,name=mtime,proto3" json:"mtime,omitempty"` + Atime int64 `protobuf:"varint,6,opt,name=atime,proto3" json:"atime,omitempty"` + Ctime int64 `protobuf:"varint,7,opt,name=ctime,proto3" json:"ctime,omitempty"` + Btime int64 `protobuf:"varint,8,opt,name=btime,proto3" json:"btime,omitempty"` + Mode int64 `protobuf:"varint,9,opt,name=mode,proto3" json:"mode,omitempty"` + StartOffset int64 `protobuf:"varint,10,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"` + UploadId int64 `protobuf:"varint,11,opt,name=upload_id,json=uploadId,proto3" json:"upload_id,omitempty"` + // A JSON object that represents the result of the upload + Response string `protobuf:"bytes,12,opt,name=response,proto3" json:"response,omitempty"` +} + +func (x *UploadTransaction) Reset() { + *x = UploadTransaction{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UploadTransaction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UploadTransaction) ProtoMessage() {} + +func (x *UploadTransaction) ProtoReflect() protoreflect.Message { + mi := &file_transport_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UploadTransaction.ProtoReflect.Descriptor instead. +func (*UploadTransaction) Descriptor() ([]byte, []int) { + return file_transport_proto_rawDescGZIP(), []int{5} +} + +func (x *UploadTransaction) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *UploadTransaction) GetAccessor() string { + if x != nil { + return x.Accessor + } + return "" +} + +func (x *UploadTransaction) GetStoreAsName() string { + if x != nil { + return x.StoreAsName + } + return "" +} + +func (x *UploadTransaction) GetComponents() []string { + if x != nil { + return x.Components + } + return nil +} + +func (x *UploadTransaction) GetExpectedSize() int64 { + if x != nil { + return x.ExpectedSize + } + return 0 +} + +func (x *UploadTransaction) GetMtime() int64 { + if x != nil { + return x.Mtime + } + return 0 +} + +func (x *UploadTransaction) GetAtime() int64 { + if x != nil { + return x.Atime + } + return 0 +} + +func (x *UploadTransaction) GetCtime() int64 { + if x != nil { + return x.Ctime + } + return 0 +} + +func (x *UploadTransaction) GetBtime() int64 { + if x != nil { + return x.Btime + } + return 0 +} + +func (x *UploadTransaction) GetMode() int64 { + if x != nil { + return x.Mode + } + return 0 +} + +func (x *UploadTransaction) GetStartOffset() int64 { + if x != nil { + return x.StartOffset + } + return 0 +} + +func (x *UploadTransaction) GetUploadId() int64 { + if x != nil { + return x.UploadId + } + return 0 +} + +func (x *UploadTransaction) GetResponse() string { + if x != nil { + return x.Response + } + return "" +} + var File_transport_proto protoreflect.FileDescriptor var file_transport_proto_rawDesc = []byte{ @@ -418,7 +624,7 @@ var file_transport_proto_rawDesc = []byte{ 0x01, 0x28, 0x03, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0x2d, 0x0a, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x24, 0x0a, 0x06, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x61, 0x6e, - 0x67, 0x65, 0x52, 0x06, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x22, 0xdb, 0x01, 0x0a, 0x08, 0x50, + 0x67, 0x65, 0x52, 0x06, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x22, 0xfb, 0x01, 0x0a, 0x08, 0x50, 0x61, 0x74, 0x68, 0x53, 0x70, 0x65, 0x63, 0x12, 0x81, 0x01, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x6d, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x67, 0x12, 0x65, 0x54, 0x68, 0x65, 0x20, 0x70, 0x61, 0x74, 0x68, 0x20, 0x70, 0x61, 0x73, 0x73, 0x65, 0x64, 0x20, @@ -427,12 +633,14 @@ var file_transport_proto_rawDesc = []byte{ 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x65, 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x20, 0x69, 0x6e, 0x20, 0x69, 0x74, 0x73, 0x20, 0x6f, 0x77, 0x6e, - 0x20, 0x77, 0x61, 0x79, 0x2e, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x4b, 0x0a, 0x08, 0x61, + 0x20, 0x77, 0x61, 0x79, 0x2e, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1e, 0x0a, 0x0a, 0x63, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x4b, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x29, 0x12, 0x27, 0x54, 0x68, 0x65, 0x20, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x20, 0x75, 0x73, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x08, - 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x22, 0x89, 0x03, 0x0a, 0x0a, 0x46, 0x69, 0x6c, + 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x22, 0x9e, 0x04, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x08, 0x70, 0x61, 0x74, 0x68, 0x73, 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x53, 0x70, 0x65, 0x63, 0x52, 0x08, 0x70, 0x61, 0x74, 0x68, @@ -446,29 +654,63 @@ var file_transport_proto_rawDesc = []byte{ 0x52, 0x0a, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x73, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x53, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x17, 0x0a, - 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6f, 0x66, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x03, 0x65, 0x6f, 0x66, 0x12, 0x22, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, - 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x14, 0x0a, 0x05, - 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6d, 0x74, 0x69, - 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x62, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, - 0x74, 0x69, 0x6d, 0x65, 0x22, 0x79, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x65, 0x6d, 0x61, 0x6e, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, - 0x75, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x37, 0x0a, 0x18, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, - 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x15, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, - 0x35, 0x5a, 0x33, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, - 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2f, 0x0a, + 0x13, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x6c, 0x65, + 0x6e, 0x67, 0x74, 0x68, 0x18, 0x11, 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x75, 0x6e, 0x63, 0x6f, + 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x1f, + 0x0a, 0x0b, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x10, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, + 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6f, 0x66, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x65, 0x6f, 0x66, 0x12, 0x22, 0x0a, 0x05, 0x69, 0x6e, + 0x64, 0x65, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x14, + 0x0a, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6d, + 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x62, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x05, 0x62, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, + 0x65, 0x6e, 0x63, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x6e, + 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x75, 0x70, 0x6c, + 0x6f, 0x61, 0x64, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x79, 0x0a, 0x0e, 0x46, 0x6f, 0x72, + 0x65, 0x6d, 0x61, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x6c, + 0x61, 0x73, 0x74, 0x5f, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x75, + 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x37, 0x0a, 0x18, 0x6c, + 0x61, 0x73, 0x74, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x15, 0x6c, + 0x61, 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xfc, 0x02, 0x0a, 0x11, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, + 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, + 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x6f, 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x61, 0x73, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x41, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, + 0x65, 0x6e, 0x74, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x70, + 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, + 0x65, 0x64, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x65, + 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6d, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6d, 0x74, 0x69, 0x6d, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x62, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x74, + 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x75, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x75, + 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x42, 0x35, 0x5a, 0x33, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, + 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, + 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -483,13 +725,14 @@ func file_transport_proto_rawDescGZIP() []byte { return file_transport_proto_rawDescData } -var file_transport_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_transport_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_transport_proto_goTypes = []interface{}{ - (*Range)(nil), // 0: proto.Range - (*Index)(nil), // 1: proto.Index - (*PathSpec)(nil), // 2: proto.PathSpec - (*FileBuffer)(nil), // 3: proto.FileBuffer - (*ForemanCheckin)(nil), // 4: proto.ForemanCheckin + (*Range)(nil), // 0: proto.Range + (*Index)(nil), // 1: proto.Index + (*PathSpec)(nil), // 2: proto.PathSpec + (*FileBuffer)(nil), // 3: proto.FileBuffer + (*ForemanCheckin)(nil), // 4: proto.ForemanCheckin + (*UploadTransaction)(nil), // 5: proto.UploadTransaction } var file_transport_proto_depIdxs = []int32{ 0, // 0: proto.Index.ranges:type_name -> proto.Range @@ -568,6 +811,18 @@ func file_transport_proto_init() { return nil } } + file_transport_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UploadTransaction); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -575,7 +830,7 @@ func file_transport_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_transport_proto_rawDesc, NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/actions/proto/transport.proto b/actions/proto/transport.proto index c53a1485f..beece0d46 100644 --- a/actions/proto/transport.proto +++ b/actions/proto/transport.proto @@ -8,9 +8,17 @@ package proto; option go_package = "www.velocidex.com/golang/velociraptor/actions/proto"; message Range { + // Range offset in the underlying file. int64 file_offset = 1; + + // Range offset in the underlying file int64 original_offset = 2; + + // The length of data that exists in the underlying file. May be 0 + // if the range is sparse and has no underlying storage. int64 file_length = 3; + + // Length of this range. int64 length = 4; } @@ -26,6 +34,8 @@ message PathSpec { "This value is interpreted by the accessor in its own way.", }]; + repeated string components = 4; + string accessor = 3 [(sem_type) = { description: "The accessor used to retrieve the file.", }]; @@ -48,6 +58,17 @@ message FileBuffer { bool is_sparse = 9; bytes data = 3; + + // If this is > 0 then then data field contains already compressed + // data. The length of the uncompressed data will be given here. + uint64 uncompressed_length = 17; + + // For uploaders that do not transfer the bulk data inside the + // data field, we need a way to specify how much data was + // transferred in thie buffer. In the usual uploader this will be + // the len(data). + uint64 data_length = 16; + string flow_id = 4; bool eof = 5; @@ -58,9 +79,45 @@ message FileBuffer { int64 atime = 11; int64 ctime = 12; int64 btime = 13; + + // Set when the actual file is stored somewhere else (e.g. S3) + string reference = 14; + + // An incrementing number of uploads across the entire + // collection. Velociraptor file uploads are stored per collection + // and not per query so this number is unique across all the + // queries in the collection. It amounts to the row id on the + // collections uploads result set. + int64 upload_number = 15; } message ForemanCheckin { uint64 last_hunt_timestamp = 1; uint64 last_event_table_version = 2; } + +// An UploadTransaction represents an intention for the client to +// begin an upload. The upload will proceed in the future. The actual +// upload may be cancelled or timed out and then can be resumed by the +// user. +// +// The goal of this message is to capture state as much as possible to +// allow the upload to resume, i.e. reflect the uploader interface +// args. +message UploadTransaction { + string filename = 1; + string accessor = 2; + string store_as_name = 3; + repeated string components = 13; + int64 expected_size = 4; + int64 mtime = 5; + int64 atime = 6; + int64 ctime = 7; + int64 btime = 8; + int64 mode = 9; + int64 start_offset = 10; + int64 upload_id = 11; + + // A JSON object that represents the result of the upload + string response = 12; +} diff --git a/actions/proto/vql.pb.go b/actions/proto/vql.pb.go index 64c61d9d6..1747b077e 100644 --- a/actions/proto/vql.pb.go +++ b/actions/proto/vql.pb.go @@ -1,9 +1,6 @@ // These are the messages used in client actions. // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: vql.proto package proto @@ -29,11 +26,10 @@ type VQLRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // DEPRECATED: Will be populated for compatibility with older clients. + // The obfuscated name of the artifact this query came from. Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"` - // DEPRECATED: Not used any more. - Description string `protobuf:"bytes,3,opt,name=Description,proto3" json:"Description,omitempty"` - VQL string `protobuf:"bytes,1,opt,name=VQL,proto3" json:"VQL,omitempty"` + // The compiled VQL query to evaluate on the endpoint. + VQL string `protobuf:"bytes,1,opt,name=VQL,proto3" json:"VQL,omitempty"` } func (x *VQLRequest) Reset() { @@ -75,13 +71,6 @@ func (x *VQLRequest) GetName() string { return "" } -func (x *VQLRequest) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - func (x *VQLRequest) GetVQL() string { if x != nil { return x.VQL @@ -94,8 +83,9 @@ type VQLEnv struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + Comment string `protobuf:"bytes,3,opt,name=comment,proto3" json:"comment,omitempty"` } func (x *VQLEnv) Reset() { @@ -144,6 +134,13 @@ func (x *VQLEnv) GetValue() string { return "" } +func (x *VQLEnv) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + // This is the most common type of message - it specifies a query to // run on the endpoint. type VQLCollectorArgs struct { @@ -151,23 +148,43 @@ type VQLCollectorArgs struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + // Number of this request compared to the entire collection. + QueryId int64 `protobuf:"varint,32,opt,name=query_id,json=queryId,proto3" json:"query_id,omitempty"` + TotalQueries int64 `protobuf:"varint,33,opt,name=total_queries,json=totalQueries,proto3" json:"total_queries,omitempty"` + // If set we ignore any requests older than this. + Expiry uint64 `protobuf:"varint,34,opt,name=expiry,proto3" json:"expiry,omitempty"` // If this is specified we run this query first and if it returns // any rows we continue with the real query. Precondition string `protobuf:"bytes,29,opt,name=precondition,proto3" json:"precondition,omitempty"` // The principal that created this request - only sent when // scheduling the server so the server may keep track on who ran // each query. - Principal string `protobuf:"bytes,28,opt,name=principal,proto3" json:"principal,omitempty"` - Env []*VQLEnv `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty"` - Query []*VQLRequest `protobuf:"bytes,2,rep,name=Query,proto3" json:"Query,omitempty"` - MaxRow uint64 `protobuf:"varint,4,opt,name=max_row,json=maxRow,proto3" json:"max_row,omitempty"` - MaxWait uint64 `protobuf:"varint,6,opt,name=max_wait,json=maxWait,proto3" json:"max_wait,omitempty"` - OpsPerSecond float32 `protobuf:"fixed32,24,opt,name=ops_per_second,json=opsPerSecond,proto3" json:"ops_per_second,omitempty"` - Artifacts []*proto.Artifact `protobuf:"bytes,5,rep,name=artifacts,proto3" json:"artifacts,omitempty"` - Timeout uint64 `protobuf:"varint,25,opt,name=timeout,proto3" json:"timeout,omitempty"` + Principal string `protobuf:"bytes,28,opt,name=principal,proto3" json:"principal,omitempty"` + // The effective ACLs that will be used for this query. This is + // usually set by the artifact's suid field. + EffectivePrincipal string `protobuf:"bytes,38,opt,name=effective_principal,json=effectivePrincipal,proto3" json:"effective_principal,omitempty"` + Env []*VQLEnv `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty"` + Query []*VQLRequest `protobuf:"bytes,2,rep,name=Query,proto3" json:"Query,omitempty"` + MaxRow uint64 `protobuf:"varint,4,opt,name=max_row,json=maxRow,proto3" json:"max_row,omitempty"` + // If the row buffer size gets above this size we send the payload + // anyway. This is a fairer measure than max_rows of how large the + // payload is likely to be before compression because rows come in + // all different sizes. Default value is 5mb to match the typical + // max_upload_bytes + MaxRowBufferSize uint64 `protobuf:"varint,37,opt,name=max_row_buffer_size,json=maxRowBufferSize,proto3" json:"max_row_buffer_size,omitempty"` + MaxWait uint64 `protobuf:"varint,6,opt,name=max_wait,json=maxWait,proto3" json:"max_wait,omitempty"` + // This is deprecated in favor of the below limits. + OpsPerSecond float32 `protobuf:"fixed32,24,opt,name=ops_per_second,json=opsPerSecond,proto3" json:"ops_per_second,omitempty"` + CpuLimit float32 `protobuf:"fixed32,30,opt,name=cpu_limit,json=cpuLimit,proto3" json:"cpu_limit,omitempty"` + IopsLimit float32 `protobuf:"fixed32,31,opt,name=iops_limit,json=iopsLimit,proto3" json:"iops_limit,omitempty"` + ProgressTimeout float32 `protobuf:"fixed32,36,opt,name=progress_timeout,json=progressTimeout,proto3" json:"progress_timeout,omitempty"` + Artifacts []*proto.Artifact `protobuf:"bytes,5,rep,name=artifacts,proto3" json:"artifacts,omitempty"` + Timeout uint64 `protobuf:"varint,25,opt,name=timeout,proto3" json:"timeout,omitempty"` // How often to heart beat progress (default 30 sec) Heartbeat uint64 `protobuf:"varint,27,opt,name=heartbeat,proto3" json:"heartbeat,omitempty"` Tools []string `protobuf:"bytes,26,rep,name=tools,proto3" json:"tools,omitempty"` + // Used only for API based calls + OrgId string `protobuf:"bytes,35,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` } func (x *VQLCollectorArgs) Reset() { @@ -202,6 +219,27 @@ func (*VQLCollectorArgs) Descriptor() ([]byte, []int) { return file_vql_proto_rawDescGZIP(), []int{2} } +func (x *VQLCollectorArgs) GetQueryId() int64 { + if x != nil { + return x.QueryId + } + return 0 +} + +func (x *VQLCollectorArgs) GetTotalQueries() int64 { + if x != nil { + return x.TotalQueries + } + return 0 +} + +func (x *VQLCollectorArgs) GetExpiry() uint64 { + if x != nil { + return x.Expiry + } + return 0 +} + func (x *VQLCollectorArgs) GetPrecondition() string { if x != nil { return x.Precondition @@ -216,6 +254,13 @@ func (x *VQLCollectorArgs) GetPrincipal() string { return "" } +func (x *VQLCollectorArgs) GetEffectivePrincipal() string { + if x != nil { + return x.EffectivePrincipal + } + return "" +} + func (x *VQLCollectorArgs) GetEnv() []*VQLEnv { if x != nil { return x.Env @@ -237,6 +282,13 @@ func (x *VQLCollectorArgs) GetMaxRow() uint64 { return 0 } +func (x *VQLCollectorArgs) GetMaxRowBufferSize() uint64 { + if x != nil { + return x.MaxRowBufferSize + } + return 0 +} + func (x *VQLCollectorArgs) GetMaxWait() uint64 { if x != nil { return x.MaxWait @@ -251,6 +303,27 @@ func (x *VQLCollectorArgs) GetOpsPerSecond() float32 { return 0 } +func (x *VQLCollectorArgs) GetCpuLimit() float32 { + if x != nil { + return x.CpuLimit + } + return 0 +} + +func (x *VQLCollectorArgs) GetIopsLimit() float32 { + if x != nil { + return x.IopsLimit + } + return 0 +} + +func (x *VQLCollectorArgs) GetProgressTimeout() float32 { + if x != nil { + return x.ProgressTimeout + } + return 0 +} + func (x *VQLCollectorArgs) GetArtifacts() []*proto.Artifact { if x != nil { return x.Artifacts @@ -279,6 +352,13 @@ func (x *VQLCollectorArgs) GetTools() []string { return nil } +func (x *VQLCollectorArgs) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + type VQLTypeMap struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -342,15 +422,26 @@ type VQLResponse struct { // DEPRECATED: Response is encoded in a json array of rows. Response string `protobuf:"bytes,1,opt,name=Response,proto3" json:"Response,omitempty"` // Response is encoded as line delimited JSON. - JSONLResponse string `protobuf:"bytes,10,opt,name=JSONLResponse,proto3" json:"JSONLResponse,omitempty"` - Columns []string `protobuf:"bytes,2,rep,name=Columns,proto3" json:"Columns,omitempty"` - Types []*VQLTypeMap `protobuf:"bytes,8,rep,name=types,proto3" json:"types,omitempty"` - QueryId uint64 `protobuf:"varint,5,opt,name=query_id,json=queryId,proto3" json:"query_id,omitempty"` - Part uint64 `protobuf:"varint,6,opt,name=part,proto3" json:"part,omitempty"` - Query *VQLRequest `protobuf:"bytes,3,opt,name=Query,proto3" json:"Query,omitempty"` - Timestamp uint64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - TotalRows uint64 `protobuf:"varint,7,opt,name=total_rows,json=totalRows,proto3" json:"total_rows,omitempty"` - Log string `protobuf:"bytes,9,opt,name=log,proto3" json:"log,omitempty"` + JSONLResponse string `protobuf:"bytes,10,opt,name=JSONLResponse,proto3" json:"JSONLResponse,omitempty"` + CompressedJsonResponse []byte `protobuf:"bytes,15,opt,name=CompressedJsonResponse,proto3" json:"CompressedJsonResponse,omitempty"` + // If uncompressed_size > 0 then the above JSONLResponse is + // compressed and this field contains the uncompressed size. + UncompressedSize uint64 `protobuf:"varint,13,opt,name=uncompressed_size,json=uncompressedSize,proto3" json:"uncompressed_size,omitempty"` + // The offset in the file stream of the uncompressed JSONLResponse + // buffer. + ByteOffset uint64 `protobuf:"varint,14,opt,name=byte_offset,json=byteOffset,proto3" json:"byte_offset,omitempty"` + Columns []string `protobuf:"bytes,2,rep,name=Columns,proto3" json:"Columns,omitempty"` + Types []*VQLTypeMap `protobuf:"bytes,8,rep,name=types,proto3" json:"types,omitempty"` + QueryId uint64 `protobuf:"varint,5,opt,name=query_id,json=queryId,proto3" json:"query_id,omitempty"` + Part uint64 `protobuf:"varint,6,opt,name=part,proto3" json:"part,omitempty"` + Query *VQLRequest `protobuf:"bytes,3,opt,name=Query,proto3" json:"Query,omitempty"` + Timestamp uint64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + TotalRows uint64 `protobuf:"varint,7,opt,name=total_rows,json=totalRows,proto3" json:"total_rows,omitempty"` + // Row count where query started. + QueryStartRow uint64 `protobuf:"varint,11,opt,name=query_start_row,json=queryStartRow,proto3" json:"query_start_row,omitempty"` + Log string `protobuf:"bytes,9,opt,name=log,proto3" json:"log,omitempty"` + // Used only for server-server comms + OrgId string `protobuf:"bytes,12,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` } func (x *VQLResponse) Reset() { @@ -399,6 +490,27 @@ func (x *VQLResponse) GetJSONLResponse() string { return "" } +func (x *VQLResponse) GetCompressedJsonResponse() []byte { + if x != nil { + return x.CompressedJsonResponse + } + return nil +} + +func (x *VQLResponse) GetUncompressedSize() uint64 { + if x != nil { + return x.UncompressedSize + } + return 0 +} + +func (x *VQLResponse) GetByteOffset() uint64 { + if x != nil { + return x.ByteOffset + } + return 0 +} + func (x *VQLResponse) GetColumns() []string { if x != nil { return x.Columns @@ -448,6 +560,13 @@ func (x *VQLResponse) GetTotalRows() uint64 { return 0 } +func (x *VQLResponse) GetQueryStartRow() uint64 { + if x != nil { + return x.QueryStartRow + } + return 0 +} + func (x *VQLResponse) GetLog() string { if x != nil { return x.Log @@ -455,6 +574,13 @@ func (x *VQLResponse) GetLog() string { return "" } +func (x *VQLResponse) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + // FIXME: We replicate a small subset of GRR's elaborate knowledgebase // protos here because the GUI API plugins use this to construct the // GRR APIs. When we re-implement the API plugins, refactor this into @@ -566,25 +692,40 @@ type ClientInfo struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` - Hostname string `protobuf:"bytes,3,opt,name=hostname,proto3" json:"hostname,omitempty"` - Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` - System string `protobuf:"bytes,5,opt,name=system,proto3" json:"system,omitempty"` - Release string `protobuf:"bytes,6,opt,name=release,proto3" json:"release,omitempty"` - Architecture string `protobuf:"bytes,7,opt,name=architecture,proto3" json:"architecture,omitempty"` - IpAddress string `protobuf:"bytes,10,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` - Ping uint64 `protobuf:"varint,11,opt,name=ping,proto3" json:"ping,omitempty"` - PingTime string `protobuf:"bytes,19,opt,name=ping_time,json=pingTime,proto3" json:"ping_time,omitempty"` - ClientVersion string `protobuf:"bytes,12,opt,name=client_version,json=clientVersion,proto3" json:"client_version,omitempty"` - ClientName string `protobuf:"bytes,13,opt,name=client_name,json=clientName,proto3" json:"client_name,omitempty"` - FirstSeenAt uint64 `protobuf:"varint,20,opt,name=first_seen_at,json=firstSeenAt,proto3" json:"first_seen_at,omitempty"` - Labels []string `protobuf:"bytes,15,rep,name=labels,proto3" json:"labels,omitempty"` - LastInterrogateFlowId string `protobuf:"bytes,16,opt,name=last_interrogate_flow_id,json=lastInterrogateFlowId,proto3" json:"last_interrogate_flow_id,omitempty"` + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + Hostname string `protobuf:"bytes,3,opt,name=hostname,proto3" json:"hostname,omitempty"` + Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` + System string `protobuf:"bytes,5,opt,name=system,proto3" json:"system,omitempty"` + Release string `protobuf:"bytes,6,opt,name=release,proto3" json:"release,omitempty"` + Architecture string `protobuf:"bytes,7,opt,name=architecture,proto3" json:"architecture,omitempty"` + IpAddress string `protobuf:"bytes,10,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` + Ping uint64 `protobuf:"varint,11,opt,name=ping,proto3" json:"ping,omitempty"` + PingTime string `protobuf:"bytes,19,opt,name=ping_time,json=pingTime,proto3" json:"ping_time,omitempty"` + ClientVersion string `protobuf:"bytes,12,opt,name=client_version,json=clientVersion,proto3" json:"client_version,omitempty"` + ClientName string `protobuf:"bytes,13,opt,name=client_name,json=clientName,proto3" json:"client_name,omitempty"` + FirstSeenAt uint64 `protobuf:"varint,20,opt,name=first_seen_at,json=firstSeenAt,proto3" json:"first_seen_at,omitempty"` + BuildTime string `protobuf:"bytes,24,opt,name=build_time,json=buildTime,proto3" json:"build_time,omitempty"` + BuildUrl string `protobuf:"bytes,25,opt,name=build_url,json=buildUrl,proto3" json:"build_url,omitempty"` + InstallTime uint64 `protobuf:"varint,26,opt,name=install_time,json=installTime,proto3" json:"install_time,omitempty"` + Labels []string `protobuf:"bytes,15,rep,name=labels,proto3" json:"labels,omitempty"` + MacAddresses []string `protobuf:"bytes,22,rep,name=mac_addresses,json=macAddresses,proto3" json:"mac_addresses,omitempty"` + // A hint if tasks are available. This does not have to be + // accurate - checking the task queue will yield the correct + // tasks. + HasTasks bool `protobuf:"varint,27,opt,name=has_tasks,json=hasTasks,proto3" json:"has_tasks,omitempty"` + LastInterrogateFlowId string `protobuf:"bytes,16,opt,name=last_interrogate_flow_id,json=lastInterrogateFlowId,proto3" json:"last_interrogate_flow_id,omitempty"` // This can be a customized artifact that is compatible with // Generic.Client.Info LastInterrogateArtifactName string `protobuf:"bytes,21,opt,name=last_interrogate_artifact_name,json=lastInterrogateArtifactName,proto3" json:"last_interrogate_artifact_name,omitempty"` LastHuntTimestamp uint64 `protobuf:"varint,17,opt,name=last_hunt_timestamp,json=lastHuntTimestamp,proto3" json:"last_hunt_timestamp,omitempty"` LastEventTableVersion uint64 `protobuf:"varint,18,opt,name=last_event_table_version,json=lastEventTableVersion,proto3" json:"last_event_table_version,omitempty"` + LabelsTimestamp uint64 `protobuf:"varint,23,opt,name=labels_timestamp,json=labelsTimestamp,proto3" json:"labels_timestamp,omitempty"` + // A List of flows that are currently in flight and their last + // update epoch time. + InFlightFlows map[string]int64 `protobuf:"bytes,28,rep,name=in_flight_flows,json=inFlightFlows,proto3" json:"in_flight_flows,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` + // A list of indexed metadata fields. These are not all metadata + // fields, only the ones that are important enough to be indexed. + Metadata map[string]string `protobuf:"bytes,29,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *ClientInfo) Reset() { @@ -703,6 +844,27 @@ func (x *ClientInfo) GetFirstSeenAt() uint64 { return 0 } +func (x *ClientInfo) GetBuildTime() string { + if x != nil { + return x.BuildTime + } + return "" +} + +func (x *ClientInfo) GetBuildUrl() string { + if x != nil { + return x.BuildUrl + } + return "" +} + +func (x *ClientInfo) GetInstallTime() uint64 { + if x != nil { + return x.InstallTime + } + return 0 +} + func (x *ClientInfo) GetLabels() []string { if x != nil { return x.Labels @@ -710,6 +872,20 @@ func (x *ClientInfo) GetLabels() []string { return nil } +func (x *ClientInfo) GetMacAddresses() []string { + if x != nil { + return x.MacAddresses + } + return nil +} + +func (x *ClientInfo) GetHasTasks() bool { + if x != nil { + return x.HasTasks + } + return false +} + func (x *ClientInfo) GetLastInterrogateFlowId() string { if x != nil { return x.LastInterrogateFlowId @@ -738,6 +914,27 @@ func (x *ClientInfo) GetLastEventTableVersion() uint64 { return 0 } +func (x *ClientInfo) GetLabelsTimestamp() uint64 { + if x != nil { + return x.LabelsTimestamp + } + return 0 +} + +func (x *ClientInfo) GetInFlightFlows() map[string]int64 { + if x != nil { + return x.InFlightFlows + } + return nil +} + +func (x *ClientInfo) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + var File_vql_proto protoreflect.FileDescriptor var file_vql_proto_rawDesc = []byte{ @@ -745,211 +942,259 @@ var file_vql_proto_rawDesc = []byte{ 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa8, 0x02, 0x0a, 0x0a, 0x56, 0x51, 0x4c, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x88, 0x01, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x74, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x6e, 0x12, 0x6c, - 0x54, 0x68, 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, - 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x73, 0x68, 0x6f, - 0x75, 0x6c, 0x64, 0x20, 0x62, 0x65, 0x20, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x76, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x69, 0x6e, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x77, - 0x68, 0x61, 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x69, 0x6e, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x69, 0x74, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x71, 0x75, 0x65, 0x72, - 0x79, 0x20, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x73, 0x2e, 0x52, 0x04, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x24, 0x12, - 0x22, 0x57, 0x68, 0x61, 0x74, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, - 0x20, 0x69, 0x73, 0x20, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, - 0x64, 0x6f, 0x2e, 0x52, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x41, 0x0a, 0x03, 0x56, 0x51, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2f, 0xe2, - 0xfc, 0xe3, 0xc4, 0x01, 0x29, 0x12, 0x27, 0x54, 0x68, 0x65, 0x20, 0x56, 0x51, 0x4c, 0x20, 0x71, - 0x75, 0x65, 0x72, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x20, - 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x03, - 0x56, 0x51, 0x4c, 0x22, 0x30, 0x0a, 0x06, 0x56, 0x51, 0x4c, 0x45, 0x6e, 0x76, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xe8, 0x08, 0x0a, 0x10, 0x56, 0x51, 0x4c, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, 0x72, 0x67, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, - 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, - 0x0a, 0x09, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x18, 0x1c, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x12, 0x5c, 0x0a, 0x03, - 0x65, 0x6e, 0x76, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x45, 0x6e, 0x76, 0x42, 0x3b, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x35, - 0x12, 0x33, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x76, 0x61, - 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x71, - 0x75, 0x65, 0x72, 0x79, 0x2e, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x5a, 0x0a, 0x05, 0x51, 0x75, - 0x65, 0x72, 0x79, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x31, 0xe2, 0xfc, - 0xe3, 0xc4, 0x01, 0x2b, 0x12, 0x29, 0x54, 0x68, 0x65, 0x20, 0x56, 0x51, 0x4c, 0x20, 0x71, 0x75, - 0x65, 0x72, 0x69, 0x65, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, - 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x52, - 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x9d, 0x01, 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x72, - 0x6f, 0x77, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x42, 0x83, 0x01, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, - 0x7d, 0x12, 0x62, 0x54, 0x68, 0x65, 0x20, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x20, 0x72, - 0x6f, 0x77, 0x73, 0x20, 0x70, 0x65, 0x72, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x2e, 0x20, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x20, 0x6c, 0x61, 0x72, 0x67, - 0x65, 0x72, 0x20, 0x74, 0x68, 0x61, 0x6e, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x77, 0x69, 0x6c, - 0x6c, 0x20, 0x62, 0x65, 0x20, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x20, 0x61, 0x63, 0x72, 0x6f, 0x73, - 0x73, 0x20, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x73, 0x2e, 0x22, 0x11, 0x4d, 0x61, 0x78, 0x20, 0x72, 0x6f, 0x77, 0x73, 0x20, - 0x70, 0x65, 0x72, 0x20, 0x70, 0x61, 0x72, 0x74, 0x32, 0x04, 0x31, 0x30, 0x30, 0x30, 0x52, 0x06, - 0x6d, 0x61, 0x78, 0x52, 0x6f, 0x77, 0x12, 0xcb, 0x01, 0x0a, 0x08, 0x6d, 0x61, 0x78, 0x5f, 0x77, - 0x61, 0x69, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x42, 0xaf, 0x01, 0xe2, 0xfc, 0xe3, 0xc4, - 0x01, 0xa8, 0x01, 0x12, 0x7d, 0x46, 0x6f, 0x72, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x20, 0x71, 0x75, - 0x65, 0x72, 0x69, 0x65, 0x73, 0x20, 0x77, 0x65, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, - 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x20, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x20, - 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x2e, - 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, - 0x64, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x20, 0x6c, 0x69, 0x73, 0x74, - 0x65, 0x6e, 0x65, 0x72, 0x73, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, 0x6e, 0x65, 0x76, 0x65, - 0x72, 0x20, 0x72, 0x65, 0x61, 0x6c, 0x6c, 0x79, 0x20, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x2e, 0x22, 0x23, 0x42, 0x61, 0x74, 0x63, 0x68, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x74, 0x61, 0x6b, 0x65, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x2e, 0x32, 0x02, 0x31, 0x30, 0x52, 0x07, 0x6d, 0x61, 0x78, - 0x57, 0x61, 0x69, 0x74, 0x12, 0xc8, 0x01, 0x0a, 0x0e, 0x6f, 0x70, 0x73, 0x5f, 0x70, 0x65, 0x72, - 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x18, 0x18, 0x20, 0x01, 0x28, 0x02, 0x42, 0xa1, 0x01, - 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x9a, 0x01, 0x12, 0x97, 0x01, 0x41, 0x6e, 0x20, 0x4f, 0x70, 0x20, - 0x69, 0x73, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x61, 0x73, 0x20, 0x73, 0x6f, - 0x6d, 0x65, 0x20, 0x61, 0x72, 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x20, 0x75, 0x6e, 0x69, - 0x74, 0x20, 0x6f, 0x66, 0x20, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, - 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x73, 0x20, 0x77, 0x6f, 0x72, 0x6b, 0x20, 0x74, 0x6f, 0x20, 0x62, - 0x65, 0x20, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 0x2e, 0x20, 0x54, 0x79, 0x70, 0x69, 0x63, - 0x61, 0x6c, 0x6c, 0x79, 0x20, 0x56, 0x51, 0x4c, 0x20, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, - 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x20, 0x6f, 0x70, 0x73, 0x20, - 0x74, 0x6f, 0x77, 0x61, 0x72, 0x64, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x71, 0x75, 0x65, 0x72, - 0x79, 0x20, 0x61, 0x73, 0x20, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x70, 0x72, 0x69, 0x61, 0x74, 0x65, - 0x2e, 0x52, 0x0c, 0x6f, 0x70, 0x73, 0x50, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x12, - 0x6e, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x42, 0x3f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x39, 0x12, 0x37, 0x41, 0x72, 0x74, - 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x20, 0x73, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x72, 0x6f, 0x6d, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x74, 0x6f, 0x20, 0x68, - 0x65, 0x6c, 0x70, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x71, 0x75, - 0x65, 0x72, 0x79, 0x2e, 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x12, - 0x44, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x19, 0x20, 0x01, 0x28, 0x04, - 0x42, 0x2a, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x24, 0x12, 0x22, 0x4d, 0x61, 0x78, 0x69, 0x6d, 0x75, - 0x6d, 0x20, 0x74, 0x69, 0x6d, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x71, - 0x75, 0x65, 0x72, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x75, 0x6e, 0x2e, 0x52, 0x07, 0x74, 0x69, - 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, - 0x61, 0x74, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, - 0x65, 0x61, 0x74, 0x12, 0x4b, 0x0a, 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x1a, 0x20, 0x03, - 0x28, 0x09, 0x42, 0x35, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x2f, 0x12, 0x2d, 0x41, 0x20, 0x6c, 0x69, - 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x20, 0x77, 0x65, 0x20, 0x77, - 0x69, 0x6c, 0x6c, 0x20, 0x6e, 0x65, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x75, 0x6e, 0x20, - 0x74, 0x68, 0x69, 0x73, 0x20, 0x56, 0x51, 0x4c, 0x2e, 0x52, 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, - 0x22, 0x38, 0x0a, 0x0a, 0x56, 0x51, 0x4c, 0x54, 0x79, 0x70, 0x65, 0x4d, 0x61, 0x70, 0x12, 0x16, - 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xa0, 0x06, 0x0a, 0x0b, 0x56, - 0x51, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x08, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xe2, 0xfc, - 0xe3, 0xc4, 0x01, 0x18, 0x12, 0x16, 0x4a, 0x53, 0x4f, 0x4e, 0x20, 0x65, 0x6e, 0x63, 0x6f, 0x64, - 0x65, 0x64, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x08, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0d, 0x4a, 0x53, 0x4f, 0x4e, 0x4c, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xe2, - 0xfc, 0xe3, 0xc4, 0x01, 0x18, 0x12, 0x16, 0x4a, 0x53, 0x4f, 0x4e, 0x20, 0x65, 0x6e, 0x63, 0x6f, - 0x64, 0x65, 0x64, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x0d, 0x4a, - 0x53, 0x4f, 0x4e, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x07, - 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, 0x38, 0xe2, - 0xfc, 0xe3, 0xc4, 0x01, 0x32, 0x12, 0x30, 0x41, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, - 0x20, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x20, 0x68, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x73, - 0x20, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x52, 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, - 0x12, 0x5e, 0x0a, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x54, 0x79, 0x70, 0x65, 0x4d, - 0x61, 0x70, 0x42, 0x35, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x2f, 0x12, 0x2d, 0x4d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x20, 0x62, 0x65, 0x74, 0x77, 0x65, 0x65, 0x6e, 0x20, 0x63, 0x6f, 0x6c, 0x75, - 0x6d, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x74, 0x68, 0x65, - 0x69, 0x72, 0x20, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x52, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, - 0x12, 0x52, 0x0a, 0x08, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x04, 0x42, 0x37, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x31, 0x12, 0x2f, 0x43, 0x68, 0x72, 0x6f, - 0x6e, 0x6f, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x20, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x20, - 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x20, 0x77, 0x65, 0x20, - 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x20, 0x74, 0x6f, 0x2e, 0x52, 0x07, 0x71, 0x75, 0x65, - 0x72, 0x79, 0x49, 0x64, 0x12, 0x74, 0x0a, 0x04, 0x70, 0x61, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x04, 0x42, 0x60, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x5a, 0x12, 0x58, 0x4c, 0x61, 0x72, 0x67, - 0x65, 0x20, 0x56, 0x51, 0x4c, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x20, - 0x61, 0x72, 0x65, 0x20, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x20, 0x61, 0x63, 0x72, 0x6f, 0x73, 0x73, - 0x20, 0x6d, 0x61, 0x6e, 0x79, 0x20, 0x70, 0x61, 0x72, 0x74, 0x73, 0x2e, 0x20, 0x54, 0x68, 0x69, - 0x73, 0x20, 0x63, 0x61, 0x72, 0x72, 0x69, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, - 0x72, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x04, 0x70, 0x61, 0x72, 0x74, 0x12, 0x4d, 0x0a, 0x05, 0x51, 0x75, - 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x24, 0xe2, 0xfc, - 0xe3, 0xc4, 0x01, 0x1e, 0x12, 0x1c, 0x54, 0x68, 0x65, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x20, - 0x74, 0x68, 0x61, 0x74, 0x20, 0x77, 0x61, 0x73, 0x20, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, - 0x64, 0x2e, 0x52, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x5c, 0x0a, 0x09, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x42, 0x3e, 0xe2, 0xfc, - 0xe3, 0xc4, 0x01, 0x38, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, - 0x65, 0x12, 0x29, 0x54, 0x68, 0x65, 0x20, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x20, 0x77, 0x61, - 0x73, 0x20, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x52, 0x09, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x52, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, - 0x5f, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x33, 0xe2, 0xfc, 0xe3, - 0xc4, 0x01, 0x2d, 0x12, 0x2b, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x20, 0x6e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x20, 0x6f, 0x66, 0x20, 0x72, 0x6f, 0x77, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x20, 0x70, 0x61, 0x72, 0x74, 0x2e, - 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x6c, - 0x6f, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x22, 0x45, 0x0a, - 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x21, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1b, 0x12, - 0x19, 0x54, 0x68, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6f, 0x66, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, - 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xaa, 0x01, 0x0a, 0x0d, 0x56, 0x51, 0x4c, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x55, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x51, - 0x4c, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, 0x72, 0x67, 0x73, 0x42, 0x26, - 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x20, 0x12, 0x1e, 0x41, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6f, 0x66, - 0x20, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x20, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x20, 0x74, - 0x6f, 0x20, 0x72, 0x75, 0x6e, 0x2e, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x42, 0x0a, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x28, - 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x22, 0x12, 0x20, 0x54, 0x68, 0x65, 0x20, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x65, 0x76, 0x65, 0x6e, - 0x74, 0x20, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x2e, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x22, 0xea, 0x04, 0x0a, 0x0a, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, - 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, - 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x16, 0x0a, - 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, - 0x22, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, - 0x75, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x04, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x69, 0x6e, 0x67, 0x54, - 0x69, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x66, - 0x69, 0x72, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x14, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x0b, 0x66, 0x69, 0x72, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x37, 0x0a, 0x18, 0x6c, 0x61, 0x73, 0x74, 0x5f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x5f, 0x66, 0x6c, 0x6f, 0x77, - 0x5f, 0x69, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x61, 0x73, 0x74, 0x49, - 0x6e, 0x74, 0x65, 0x72, 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x6f, 0x77, 0x49, 0x64, - 0x12, 0x43, 0x0a, 0x1e, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x72, 0x6f, - 0x67, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, - 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x75, - 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x11, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x37, 0x0a, 0x18, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x76, - 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x12, 0x20, 0x01, 0x28, 0x04, 0x52, 0x15, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x35, - 0x5a, 0x33, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, - 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x32, 0x0a, 0x0a, 0x56, 0x51, 0x4c, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x56, 0x51, + 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x56, 0x51, 0x4c, 0x22, 0x4a, 0x0a, 0x06, + 0x56, 0x51, 0x4c, 0x45, 0x6e, 0x76, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x9e, 0x0b, 0x0a, 0x10, 0x56, 0x51, 0x4c, + 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, 0x72, 0x67, 0x73, 0x12, 0x19, 0x0a, + 0x08, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x20, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x07, 0x71, 0x75, 0x65, 0x72, 0x79, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x21, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x16, 0x0a, + 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x22, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, + 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x69, + 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, + 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x12, 0x2f, 0x0a, 0x13, 0x65, 0x66, 0x66, 0x65, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x18, 0x26, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x50, + 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x12, 0x5c, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x51, + 0x4c, 0x45, 0x6e, 0x76, 0x42, 0x3b, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x35, 0x12, 0x33, 0x45, 0x6e, + 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, + 0x6c, 0x65, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x64, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, + 0x2e, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x5a, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x51, + 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x31, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x2b, + 0x12, 0x29, 0x54, 0x68, 0x65, 0x20, 0x56, 0x51, 0x4c, 0x20, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, + 0x73, 0x20, 0x74, 0x6f, 0x20, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x20, 0x6f, 0x6e, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x05, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x12, 0x9d, 0x01, 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x72, 0x6f, 0x77, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x04, 0x42, 0x83, 0x01, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x7d, 0x12, 0x62, 0x54, + 0x68, 0x65, 0x20, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x20, 0x72, 0x6f, 0x77, 0x73, 0x20, + 0x70, 0x65, 0x72, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x20, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x20, 0x6c, 0x61, 0x72, 0x67, 0x65, 0x72, 0x20, 0x74, + 0x68, 0x61, 0x6e, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, + 0x20, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x20, 0x61, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x20, 0x6d, 0x75, + 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, + 0x2e, 0x22, 0x11, 0x4d, 0x61, 0x78, 0x20, 0x72, 0x6f, 0x77, 0x73, 0x20, 0x70, 0x65, 0x72, 0x20, + 0x70, 0x61, 0x72, 0x74, 0x32, 0x04, 0x31, 0x30, 0x30, 0x30, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x52, + 0x6f, 0x77, 0x12, 0x2d, 0x0a, 0x13, 0x6d, 0x61, 0x78, 0x5f, 0x72, 0x6f, 0x77, 0x5f, 0x62, 0x75, + 0x66, 0x66, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x25, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x10, 0x6d, 0x61, 0x78, 0x52, 0x6f, 0x77, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x53, 0x69, 0x7a, + 0x65, 0x12, 0xcb, 0x01, 0x0a, 0x08, 0x6d, 0x61, 0x78, 0x5f, 0x77, 0x61, 0x69, 0x74, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x04, 0x42, 0xaf, 0x01, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0xa8, 0x01, 0x12, 0x7d, + 0x46, 0x6f, 0x72, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x20, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, + 0x20, 0x77, 0x65, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x70, 0x61, 0x72, 0x74, 0x69, + 0x61, 0x6c, 0x20, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, + 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x2e, 0x20, 0x54, 0x68, 0x69, 0x73, + 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x66, 0x6f, 0x72, + 0x20, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, + 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, 0x6e, 0x65, 0x76, 0x65, 0x72, 0x20, 0x72, 0x65, 0x61, + 0x6c, 0x6c, 0x79, 0x20, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x22, 0x23, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x20, 0x74, 0x68, + 0x61, 0x74, 0x20, 0x74, 0x61, 0x6b, 0x65, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x6c, 0x6f, 0x6e, + 0x67, 0x2e, 0x32, 0x02, 0x31, 0x30, 0x52, 0x07, 0x6d, 0x61, 0x78, 0x57, 0x61, 0x69, 0x74, 0x12, + 0xc8, 0x01, 0x0a, 0x0e, 0x6f, 0x70, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x63, 0x6f, + 0x6e, 0x64, 0x18, 0x18, 0x20, 0x01, 0x28, 0x02, 0x42, 0xa1, 0x01, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, + 0x9a, 0x01, 0x12, 0x97, 0x01, 0x41, 0x6e, 0x20, 0x4f, 0x70, 0x20, 0x69, 0x73, 0x20, 0x64, 0x65, + 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x61, 0x73, 0x20, 0x73, 0x6f, 0x6d, 0x65, 0x20, 0x61, 0x72, + 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x20, 0x75, 0x6e, 0x69, 0x74, 0x20, 0x6f, 0x66, 0x20, + 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x73, 0x20, 0x77, 0x6f, 0x72, 0x6b, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x65, 0x64, 0x2e, 0x20, 0x54, 0x79, 0x70, 0x69, 0x63, 0x61, 0x6c, 0x6c, 0x79, 0x20, + 0x56, 0x51, 0x4c, 0x20, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x20, 0x77, 0x69, 0x6c, 0x6c, + 0x20, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x20, 0x6f, 0x70, 0x73, 0x20, 0x74, 0x6f, 0x77, 0x61, 0x72, + 0x64, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x20, 0x61, 0x73, 0x20, + 0x61, 0x70, 0x70, 0x72, 0x6f, 0x70, 0x72, 0x69, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x0c, 0x6f, 0x70, + 0x73, 0x50, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x70, + 0x75, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x02, 0x52, 0x08, 0x63, + 0x70, 0x75, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x6f, 0x70, 0x73, 0x5f, + 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x02, 0x52, 0x09, 0x69, 0x6f, 0x70, + 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, + 0x73, 0x73, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x24, 0x20, 0x01, 0x28, 0x02, + 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, + 0x74, 0x12, 0x6e, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, + 0x69, 0x66, 0x61, 0x63, 0x74, 0x42, 0x3f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x39, 0x12, 0x37, 0x41, + 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x20, 0x73, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x72, + 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x74, 0x6f, + 0x20, 0x68, 0x65, 0x6c, 0x70, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x73, 0x12, 0x44, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x19, 0x20, 0x01, + 0x28, 0x04, 0x42, 0x2a, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x24, 0x12, 0x22, 0x4d, 0x61, 0x78, 0x69, + 0x6d, 0x75, 0x6d, 0x20, 0x74, 0x69, 0x6d, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x75, 0x6e, 0x2e, 0x52, 0x07, + 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, + 0x62, 0x65, 0x61, 0x74, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x68, 0x65, 0x61, 0x72, + 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x4b, 0x0a, 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x1a, + 0x20, 0x03, 0x28, 0x09, 0x42, 0x35, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x2f, 0x12, 0x2d, 0x41, 0x20, + 0x6c, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x20, 0x77, 0x65, + 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x6e, 0x65, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x75, + 0x6e, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x56, 0x51, 0x4c, 0x2e, 0x52, 0x05, 0x74, 0x6f, 0x6f, + 0x6c, 0x73, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x23, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0x38, 0x0a, 0x0a, 0x56, 0x51, 0x4c, + 0x54, 0x79, 0x70, 0x65, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x22, 0xe5, 0x07, 0x0a, 0x0b, 0x56, 0x51, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x18, 0x12, 0x16, 0x4a, + 0x53, 0x4f, 0x4e, 0x20, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x64, 0x20, 0x72, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x44, 0x0a, 0x0d, 0x4a, 0x53, 0x4f, 0x4e, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x18, 0x12, 0x16, + 0x4a, 0x53, 0x4f, 0x4e, 0x20, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x64, 0x20, 0x72, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x0d, 0x4a, 0x53, 0x4f, 0x4e, 0x4c, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, + 0x73, 0x65, 0x64, 0x4a, 0x73, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, + 0x0f, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x16, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, + 0x64, 0x4a, 0x73, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, + 0x11, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, + 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x79, + 0x74, 0x65, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0a, 0x62, 0x79, 0x74, 0x65, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x52, 0x0a, 0x07, 0x43, + 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, 0x38, 0xe2, 0xfc, + 0xe3, 0xc4, 0x01, 0x32, 0x12, 0x30, 0x41, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, + 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x20, 0x68, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x20, + 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x52, 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x12, + 0x5e, 0x0a, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x54, 0x79, 0x70, 0x65, 0x4d, 0x61, + 0x70, 0x42, 0x35, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x2f, 0x12, 0x2d, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x20, 0x62, 0x65, 0x74, 0x77, 0x65, 0x65, 0x6e, 0x20, 0x63, 0x6f, 0x6c, 0x75, 0x6d, + 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x74, 0x68, 0x65, 0x69, + 0x72, 0x20, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x52, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x12, + 0x52, 0x0a, 0x08, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x04, 0x42, 0x37, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x31, 0x12, 0x2f, 0x43, 0x68, 0x72, 0x6f, 0x6e, + 0x6f, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x20, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x20, 0x6f, + 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x20, 0x77, 0x65, 0x20, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x20, 0x74, 0x6f, 0x2e, 0x52, 0x07, 0x71, 0x75, 0x65, 0x72, + 0x79, 0x49, 0x64, 0x12, 0x74, 0x0a, 0x04, 0x70, 0x61, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x04, 0x42, 0x60, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x5a, 0x12, 0x58, 0x4c, 0x61, 0x72, 0x67, 0x65, + 0x20, 0x56, 0x51, 0x4c, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x20, 0x61, + 0x72, 0x65, 0x20, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x20, 0x61, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x20, + 0x6d, 0x61, 0x6e, 0x79, 0x20, 0x70, 0x61, 0x72, 0x74, 0x73, 0x2e, 0x20, 0x54, 0x68, 0x69, 0x73, + 0x20, 0x63, 0x61, 0x72, 0x72, 0x69, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x72, + 0x74, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x52, 0x04, 0x70, 0x61, 0x72, 0x74, 0x12, 0x4d, 0x0a, 0x05, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x56, 0x51, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x24, 0xe2, 0xfc, 0xe3, + 0xc4, 0x01, 0x1e, 0x12, 0x1c, 0x54, 0x68, 0x65, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x20, 0x74, + 0x68, 0x61, 0x74, 0x20, 0x77, 0x61, 0x73, 0x20, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x64, + 0x2e, 0x52, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x5c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x42, 0x3e, 0xe2, 0xfc, 0xe3, + 0xc4, 0x01, 0x38, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, + 0x12, 0x29, 0x54, 0x68, 0x65, 0x20, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x20, 0x77, 0x61, 0x73, + 0x20, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x52, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x52, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, + 0x72, 0x6f, 0x77, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x33, 0xe2, 0xfc, 0xe3, 0xc4, + 0x01, 0x2d, 0x12, 0x2b, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x20, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, + 0x20, 0x6f, 0x66, 0x20, 0x72, 0x6f, 0x77, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x69, 0x73, + 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x20, 0x70, 0x61, 0x72, 0x74, 0x2e, 0x52, + 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x6f, 0x77, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, + 0x6f, 0x77, 0x12, 0x10, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0x45, 0x0a, 0x04, 0x55, + 0x73, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x21, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1b, 0x12, 0x19, 0x54, + 0x68, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x22, 0xaa, 0x01, 0x0a, 0x0d, 0x56, 0x51, 0x4c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, + 0x61, 0x62, 0x6c, 0x65, 0x12, 0x55, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x43, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, 0x72, 0x67, 0x73, 0x42, 0x26, 0xe2, 0xfc, + 0xe3, 0xc4, 0x01, 0x20, 0x12, 0x1e, 0x41, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x65, + 0x76, 0x65, 0x6e, 0x74, 0x20, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x20, 0x74, 0x6f, 0x20, + 0x72, 0x75, 0x6e, 0x2e, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x42, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x28, 0xe2, 0xfc, + 0xe3, 0xc4, 0x01, 0x22, 0x12, 0x20, 0x54, 0x68, 0x65, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x20, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x2e, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, + 0xc0, 0x08, 0x0a, 0x0a, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1b, + 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x68, + 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, + 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, + 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x22, 0x0a, + 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, + 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x12, 0x0a, 0x04, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, + 0x70, 0x69, 0x6e, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x69, 0x6e, 0x67, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x66, 0x69, 0x72, + 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x0b, 0x66, 0x69, 0x72, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x1d, 0x0a, + 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x18, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x19, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x55, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x73, + 0x74, 0x61, 0x6c, 0x6c, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x6c, 0x61, + 0x62, 0x65, 0x6c, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x16, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x63, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x68, 0x61, 0x73, + 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x68, 0x61, + 0x73, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x37, 0x0a, 0x18, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x5f, 0x66, 0x6c, 0x6f, 0x77, 0x5f, + 0x69, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, + 0x43, 0x0a, 0x1e, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x72, 0x6f, 0x67, + 0x61, 0x74, 0x65, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x74, + 0x65, 0x72, 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x75, 0x6e, + 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x11, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x12, 0x37, 0x0a, 0x18, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x12, 0x20, 0x01, 0x28, 0x04, 0x52, 0x15, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x0a, + 0x10, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x18, 0x17, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x4c, 0x0a, 0x0f, 0x69, 0x6e, 0x5f, 0x66, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x5f, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x18, 0x1c, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x49, 0x6e, 0x46, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x6c, 0x6f, + 0x77, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x69, 0x6e, 0x46, 0x6c, 0x69, 0x67, 0x68, + 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x1d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x1a, 0x40, 0x0a, 0x12, 0x49, 0x6e, 0x46, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, + 0x6c, 0x6f, 0x77, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x42, 0x35, 0x5a, 0x33, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, + 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, + 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -964,7 +1209,7 @@ func file_vql_proto_rawDescGZIP() []byte { return file_vql_proto_rawDescData } -var file_vql_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_vql_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_vql_proto_goTypes = []interface{}{ (*VQLRequest)(nil), // 0: proto.VQLRequest (*VQLEnv)(nil), // 1: proto.VQLEnv @@ -974,20 +1219,24 @@ var file_vql_proto_goTypes = []interface{}{ (*User)(nil), // 5: proto.User (*VQLEventTable)(nil), // 6: proto.VQLEventTable (*ClientInfo)(nil), // 7: proto.ClientInfo - (*proto.Artifact)(nil), // 8: proto.Artifact + nil, // 8: proto.ClientInfo.InFlightFlowsEntry + nil, // 9: proto.ClientInfo.MetadataEntry + (*proto.Artifact)(nil), // 10: proto.Artifact } var file_vql_proto_depIdxs = []int32{ - 1, // 0: proto.VQLCollectorArgs.env:type_name -> proto.VQLEnv - 0, // 1: proto.VQLCollectorArgs.Query:type_name -> proto.VQLRequest - 8, // 2: proto.VQLCollectorArgs.artifacts:type_name -> proto.Artifact - 3, // 3: proto.VQLResponse.types:type_name -> proto.VQLTypeMap - 0, // 4: proto.VQLResponse.Query:type_name -> proto.VQLRequest - 2, // 5: proto.VQLEventTable.event:type_name -> proto.VQLCollectorArgs - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 1, // 0: proto.VQLCollectorArgs.env:type_name -> proto.VQLEnv + 0, // 1: proto.VQLCollectorArgs.Query:type_name -> proto.VQLRequest + 10, // 2: proto.VQLCollectorArgs.artifacts:type_name -> proto.Artifact + 3, // 3: proto.VQLResponse.types:type_name -> proto.VQLTypeMap + 0, // 4: proto.VQLResponse.Query:type_name -> proto.VQLRequest + 2, // 5: proto.VQLEventTable.event:type_name -> proto.VQLCollectorArgs + 8, // 6: proto.ClientInfo.in_flight_flows:type_name -> proto.ClientInfo.InFlightFlowsEntry + 9, // 7: proto.ClientInfo.metadata:type_name -> proto.ClientInfo.MetadataEntry + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_vql_proto_init() } @@ -1099,7 +1348,7 @@ func file_vql_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_vql_proto_rawDesc, NumEnums: 0, - NumMessages: 8, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/actions/proto/vql.proto b/actions/proto/vql.proto index c60b212e8..c8388e069 100644 --- a/actions/proto/vql.proto +++ b/actions/proto/vql.proto @@ -9,27 +9,29 @@ package proto; option go_package = "www.velocidex.com/golang/velociraptor/actions/proto"; message VQLRequest { - // DEPRECATED: Will be populated for compatibility with older clients. - string Name = 2 [(sem_type) = { - description: "The name of this query. This should be descriptive to indicate what type of informaiton the query retrieves.", - }]; - // DEPRECATED: Not used any more. - string Description = 3 [(sem_type) = { - description: "What this query is supposed to do.", - }]; - string VQL = 1 [(sem_type) = { - description: "The VQL query to execute on the client.", - }]; + // The obfuscated name of the artifact this query came from. + string Name = 2; + + // The compiled VQL query to evaluate on the endpoint. + string VQL = 1; } message VQLEnv { string key = 1; string value = 2; + string comment = 3; } // This is the most common type of message - it specifies a query to // run on the endpoint. message VQLCollectorArgs { + // Number of this request compared to the entire collection. + int64 query_id = 32; + int64 total_queries = 33; + + // If set we ignore any requests older than this. + uint64 expiry = 34; + // If this is specified we run this query first and if it returns // any rows we continue with the real query. string precondition = 29; @@ -39,6 +41,10 @@ message VQLCollectorArgs { // each query. string principal = 28; + // The effective ACLs that will be used for this query. This is + // usually set by the artifact's suid field. + string effective_principal = 38; + repeated VQLEnv env = 3 [(sem_type) = { description: "Environment variables to be provided for the query.", }]; @@ -54,16 +60,28 @@ message VQLCollectorArgs { default: "1000", }]; + // If the row buffer size gets above this size we send the payload + // anyway. This is a fairer measure than max_rows of how large the + // payload is likely to be before compression because rows come in + // all different sizes. Default value is 5mb to match the typical + // max_upload_bytes + uint64 max_row_buffer_size = 37; + uint64 max_wait = 6 [(sem_type) = { friendly_name: "Batch requests that take this long.", description: "For long queries we return partial results after this long. This is required for event listeners which never really complete.", default: "10", }]; - float ops_per_second = 24 [(sem_type) = { + // This is deprecated in favor of the below limits. + float ops_per_second = 24 [(sem_type) = { description: "An Op is defined as some arbitrary unit of work. This allows work to be limited. Typically VQL plugins will count ops towards the query as appropriate." }]; + float cpu_limit = 30; + float iops_limit = 31; + float progress_timeout = 36; + repeated Artifact artifacts = 5 [(sem_type) = { description: "Artifacts sent from the server to help with this query." }]; @@ -78,6 +96,9 @@ message VQLCollectorArgs { repeated string tools = 26 [(sem_type)={ description: "A list of tools we will need to run this VQL.", }]; + + // Used only for API based calls + string org_id = 35; } message VQLTypeMap { @@ -96,6 +117,16 @@ message VQLResponse { description: "JSON encoded response.", }]; + bytes CompressedJsonResponse = 15; + + // If uncompressed_size > 0 then the above JSONLResponse is + // compressed and this field contains the uncompressed size. + uint64 uncompressed_size = 13; + + // The offset in the file stream of the uncompressed JSONLResponse + // buffer. + uint64 byte_offset = 14; + repeated string Columns = 2 [(sem_type) = { description: "A list of column headings produced by the query.", }]; @@ -112,6 +143,7 @@ message VQLResponse { description: "Large VQL responses are split across many parts. " "This carries the part of this response.", }]; + VQLRequest Query = 3 [(sem_type) = { description: "The query that was executed.", }]; @@ -125,7 +157,13 @@ message VQLResponse { description: "Total number of rows in this response part." }]; + // Row count where query started. + uint64 query_start_row = 11; + string log = 9; + + // Used only for server-server comms + string org_id = 12; } @@ -163,8 +201,17 @@ message ClientInfo { string client_version = 12; string client_name = 13; uint64 first_seen_at = 20; + string build_time = 24; + string build_url = 25; + uint64 install_time = 26; repeated string labels = 15; + repeated string mac_addresses = 22; + + // A hint if tasks are available. This does not have to be + // accurate - checking the task queue will yield the correct + // tasks. + bool has_tasks = 27; string last_interrogate_flow_id = 16; @@ -174,4 +221,14 @@ message ClientInfo { uint64 last_hunt_timestamp = 17; uint64 last_event_table_version = 18; + uint64 labels_timestamp = 23; + + // A List of flows that are currently in flight and their last + // update epoch time. + map in_flight_flows = 28; + + + // A list of indexed metadata fields. These are not all metadata + // fields, only the ones that are important enough to be indexed. + map metadata = 29; } \ No newline at end of file diff --git a/actions/query_log.go b/actions/query_log.go index 12bcce342..3f279a1a5 100644 --- a/actions/query_log.go +++ b/actions/query_log.go @@ -1,8 +1,17 @@ +/* + +Keeps an in memory log of recent queries that can be used for +debugging the client. + +*/ + package actions import ( "sync" "time" + + "www.velocidex.com/golang/velociraptor/utils" ) var ( @@ -16,11 +25,11 @@ type QueryLogEntry struct { Duration int64 } -func (self *QueryLogEntry) Copy() QueryLogEntry { +func (self *QueryLogEntry) Copy() *QueryLogEntry { self.mu.Lock() defer self.mu.Unlock() - return QueryLogEntry{ + return &QueryLogEntry{ Query: self.Query, Start: self.Start, Duration: self.Duration, @@ -31,12 +40,27 @@ func (self *QueryLogEntry) Close() { self.mu.Lock() defer self.mu.Unlock() - self.Duration = time.Now().UnixNano() - self.Start.UnixNano() + // Query was already closed - allow Close to be called multiple + // times. + if self.Duration > 0 { + return + } + + self.Duration = utils.Now().UnixNano() - self.Start.UnixNano() + + // We represent Duration == 0 as not yet complete but sometimes + // the query is closed so fast that self.Duration above is still + // zero. Account for this and make it 1. + if self.Duration == 0 { + self.Duration = 1 + } } type QueryLogType struct { mu sync.Mutex + limit int + Queries []*QueryLogEntry } @@ -52,24 +76,36 @@ func (self *QueryLogType) AddQuery(query string) *QueryLogEntry { q := &QueryLogEntry{ Query: query, - Start: time.Now(), + Start: utils.Now(), } self.Queries = append(self.Queries, q) - if len(self.Queries) > 50 { - self.Queries = self.Queries[1:] + if len(self.Queries) > self.limit { + // Drop the first finished message. This should keep the + // queries that are in flight in the queue as much as + // possible. + dropped := false + new_queries := make([]*QueryLogEntry, 0, len(self.Queries)) + for _, i := range self.Queries { + if !dropped && i.Duration != 0 { + dropped = true + } else { + new_queries = append(new_queries, i) + } + } + self.Queries = new_queries } return q } -func (self *QueryLogType) Get() []QueryLogEntry { +func (self *QueryLogType) Get() []*QueryLogEntry { self.mu.Lock() defer self.mu.Unlock() // Return a copy of the logs - result := make([]QueryLogEntry, 0, len(self.Queries)) + result := make([]*QueryLogEntry, 0, len(self.Queries)) for _, q := range self.Queries { result = append(result, q.Copy()) } @@ -78,5 +114,7 @@ func (self *QueryLogType) Get() []QueryLogEntry { } func NewQueryLog() *QueryLogType { - return &QueryLogType{} + return &QueryLogType{ + limit: 100, + } } diff --git a/actions/tracker.go b/actions/tracker.go new file mode 100644 index 000000000..f2d33b4f9 --- /dev/null +++ b/actions/tracker.go @@ -0,0 +1,43 @@ +package actions + +import ( + "sync" + + actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" +) + +// Track the row index from the beginning of the query to report to +// the server. This makes it possible for the server to not maintain +// the row index of result sets and just use this from the client. + +// The server's result set location depends on the query name, so we +// maintain a separate count for the different query names and attach +// the row index to the VQLResponse packets +type QueryTracker struct { + mu sync.Mutex + + queriesToStartRow map[string]uint64 +} + +func (self *QueryTracker) GetStartRow(query *actions_proto.VQLRequest) uint64 { + self.mu.Lock() + defer self.mu.Unlock() + + start_row, _ := self.queriesToStartRow[query.Name] + return start_row +} + +func (self *QueryTracker) AddRows( + query *actions_proto.VQLRequest, count uint64) { + self.mu.Lock() + defer self.mu.Unlock() + + start_row, _ := self.queriesToStartRow[query.Name] + self.queriesToStartRow[query.Name] = start_row + count +} + +func NewQueryTracker() *QueryTracker { + return &QueryTracker{ + queriesToStartRow: make(map[string]uint64), + } +} diff --git a/actions/transactions.go b/actions/transactions.go new file mode 100644 index 000000000..555b8b52c --- /dev/null +++ b/actions/transactions.go @@ -0,0 +1,113 @@ +package actions + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/Velocidex/ordereddict" + actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" + crypto_proto "www.velocidex.com/golang/velociraptor/crypto/proto" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/responder" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/uploads" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" +) + +// ResumeTransactions replays transactions through the client uploader +// in order to resume uploads. The results are added to the original +// flow, and any additional logs are also appended to the original +// flow. +func ResumeTransactions( + ctx context.Context, + config_obj *config_proto.Config, + responder responder.Responder, + stat *crypto_proto.VeloStatus, + req *crypto_proto.ResumeTransactions) { + + defer responder.Return(ctx) + + timeout := req.Timeout + if timeout == 0 { + timeout = 600 + } + + manager, err := services.GetRepositoryManager(config_obj) + if err != nil { + responder.RaiseError(ctx, fmt.Sprintf("%v", err)) + return + } + + repository := manager.NewRepository() + + logger := log.New(NewLogWriter(ctx, config_obj, responder), "", 0) + uploader := uploads.NewVelociraptorUploader(ctx, logger, + time.Duration(timeout)*time.Second, responder) + defer uploader.Close() + + builder := services.ScopeBuilder{ + Config: &config_proto.Config{ + Client: config_obj.Client, + Remappings: config_obj.Remappings, + }, + Ctx: ctx, + + // Only provide the client config since we are running in + // client context. + ClientConfig: config_obj.Client, + // Disable ACLs on the client. + ACLManager: acl_managers.NullACLManager{}, + Env: ordereddict.NewDict(). + // Make the session id available in the query. + Set("_SessionId", responder.FlowContext().SessionId()). + Set(constants.SCOPE_RESPONDER, responder), + Uploader: uploader, + Repository: repository, + Logger: logger, + } + + scope := manager.BuildScope(builder) + defer scope.Close() + + // Uploader needs an active scope so it needs to close before the + // scope is destroyed because it still needs to use transaction + // scopes. + defer uploader.Close() + + scope.Log("INFO:Resuming uploads: %v transactions.", len(req.Transactions)) + + var rows []*ordereddict.Dict + + for _, t := range req.Transactions { + row := ordereddict.NewDict(). + Set("ReplayTime", utils.GetTime().Now()) + row.MergeFrom(json.ConvertProtoToOrderedDict(t)) + rows = append(rows, row) + + uploader.ReplayTransaction(ctx, scope, t) + } + + jsonl, err := json.MarshalJsonl(rows) + if err == nil && len(rows) > 0 { + response := &actions_proto.VQLResponse{ + Query: &actions_proto.VQLRequest{ + Name: constants.UPLOAD_RESUMED_SOURCE, + }, + JSONLResponse: string(jsonl), + TotalRows: uint64(len(rows)), + QueryStartRow: uint64(stat.ResultRows), + Timestamp: uint64(utils.Now().UTC().UnixNano() / 1000), + Columns: rows[0].Keys(), + } + responder.AddResponse(&crypto_proto.VeloMessage{ + VQLResponse: response}) + } + + // Wait here until the uploader is done. + uploader.Close() +} diff --git a/actions/transactions_test.go b/actions/transactions_test.go new file mode 100644 index 000000000..6e21b0820 --- /dev/null +++ b/actions/transactions_test.go @@ -0,0 +1,80 @@ +package actions_test + +import ( + "os" + + "www.velocidex.com/golang/velociraptor/actions" + actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" + crypto_proto "www.velocidex.com/golang/velociraptor/crypto/proto" + "www.velocidex.com/golang/velociraptor/responder" + "www.velocidex.com/golang/velociraptor/utils/tempfile" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + + _ "www.velocidex.com/golang/velociraptor/accessors/file" +) + +func (self *ClientVQLTestSuite) TestTransactions() { + test_str := "Hello world" + + tmpfile, err := tempfile.TempFile("") + assert.NoError(self.T(), err) + tmpfile.Write([]byte(test_str)) + tmpfile.Close() + + defer os.Remove(tmpfile.Name()) + + flow_id := "F.TestTransactions" + client_id := "C.1234" + + resp := responder.TestResponderWithFlowId(self.ConfigObj, flow_id) + + stat := &crypto_proto.VeloStatus{} + + actions.ResumeTransactions(self.Sm.Ctx, self.ConfigObj, resp, stat, + &crypto_proto.ResumeTransactions{ + FlowId: flow_id, + ClientId: client_id, + Transactions: []*actions_proto.UploadTransaction{{ + Filename: tmpfile.Name(), + Accessor: "file", + // Resume upload from byte 2 + StartOffset: 2, + }}, + QueryStats: []*crypto_proto.VeloStatus{}, + }) + + responses := resp.Drain.WaitForCompletion(self.T()) + assert.True(self.T(), len(responses) > 0) + + // Should send back a standard VQLResponse into the special + // Server.Internal.ResumedUploads psuedo artifact. + assert.Contains(self.T(), getVQLResponse(responses), "ReplayTime") + assert.Contains(self.T(), getVQLResponse(responses), + "Server.Internal.ResumedUploads") + + // The completed transaction is sent to the server with a response + // field. + assert.Contains(self.T(), getUploadTransaction(responses), + `"response":"{`) + + // The response field contains a hash to signify it is complted. + assert.Contains(self.T(), getUploadTransaction(responses), + `"sha256\":\"`) + + // We also send a log to the flow to indicate the transactions are + // resumed. + assert.Contains(self.T(), getLogs(responses), + "Resuming uploads: 1 transactions.") + + // The data sent is actually from offset 2 (Hello World)[2:] + assert.Contains(self.T(), getFileBuffer(responses), + `Data: 'llo world'`) + + // The file buffer offset should start at offset 2 + assert.Contains(self.T(), getFileBuffer(responses), + `Offset: 2`) + + // Upload is completed so an EOF is sent + assert.Contains(self.T(), getFileBuffer(responses), + `EOF: true`) +} diff --git a/actions/utils_test.go b/actions/utils_test.go new file mode 100644 index 000000000..cde331f3c --- /dev/null +++ b/actions/utils_test.go @@ -0,0 +1,60 @@ +package actions_test + +import ( + "fmt" + + crypto_proto "www.velocidex.com/golang/velociraptor/crypto/proto" + "www.velocidex.com/golang/velociraptor/json" +) + +// Format various response packets so they can be better matched for +// tests. +func getLogs(responses []*crypto_proto.VeloMessage) string { + result := "" + for _, item := range responses { + if item.LogMessage != nil { + result += item.LogMessage.Jsonl + "\n" + } + } + + return result +} + +func getUploadTransaction(responses []*crypto_proto.VeloMessage) string { + result := "" + for _, item := range responses { + if item.UploadTransaction != nil { + result += json.MustMarshalString(item.UploadTransaction) + } + } + + return result +} + +func getFileBuffer(responses []*crypto_proto.VeloMessage) string { + result := "" + for _, item := range responses { + if item.FileBuffer != nil { + result += fmt.Sprintf( + "Offset: %v, Data: '%v' Data Length: %v EOF: %v\n", + item.FileBuffer.Offset, + string(item.FileBuffer.Data), + item.FileBuffer.DataLength, + item.FileBuffer.Eof) + } + } + + return result +} + +func getVQLResponse(responses []*crypto_proto.VeloMessage) string { + for _, item := range responses { + if item.VQLResponse != nil { + return fmt.Sprintf("Target: %v, JSONL: %v\n", + item.VQLResponse.Query.Name, + item.VQLResponse.JSONLResponse) + } + } + + return "" +} diff --git a/actions/vql.go b/actions/vql.go index e2c85ae46..85777f1ef 100644 --- a/actions/vql.go +++ b/actions/vql.go @@ -1,23 +1,24 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package actions import ( + "bytes" "context" "fmt" "log" @@ -31,24 +32,43 @@ import ( humanize "github.com/dustin/go-humanize" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" crypto_proto "www.velocidex.com/golang/velociraptor/crypto/proto" + "www.velocidex.com/golang/velociraptor/executor/throttler" "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/responder" "www.velocidex.com/golang/velociraptor/services" "www.velocidex.com/golang/velociraptor/uploads" + "www.velocidex.com/golang/velociraptor/utils" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/types" ) type LogWriter struct { config_obj *config_proto.Config - responder *responder.Responder + responder responder.Responder ctx context.Context } +func NewLogWriter( + ctx context.Context, + config_obj *config_proto.Config, + responder responder.Responder) *LogWriter { + return &LogWriter{ + ctx: ctx, + config_obj: config_obj, + responder: responder, + } +} + func (self *LogWriter) Write(b []byte) (int, error) { - logging.GetLogger(self.config_obj, &logging.ClientComponent).Info("%v", string(b)) - self.responder.Log(self.ctx, "%s", string(b)) + level, msg := logging.SplitIntoLevelAndLog(b) + + self.responder.Log(self.ctx, level, msg) + logging.GetLogger(self.config_obj, &logging.ClientComponent). + LogWithLevel(level, "%v", msg) return len(b), nil } @@ -57,9 +77,17 @@ type VQLClientAction struct{} func (self VQLClientAction) StartQuery( config_obj *config_proto.Config, ctx context.Context, - responder *responder.Responder, + responder responder.Responder, arg *actions_proto.VQLCollectorArgs) { + defer responder.Return(ctx) + + // Just ignore requests that are too old. + if arg.Expiry > 0 && arg.Expiry < uint64(utils.Now().Unix()) { + responder.RaiseError(ctx, "Query expired.") + return + } + // Set reasonable defaults. max_wait := arg.MaxWait if max_wait == 0 { @@ -75,11 +103,15 @@ func (self VQLClientAction) StartQuery( max_row = 10000 } - rate := arg.OpsPerSecond - if rate == 0 { - rate = 1000000 + max_row_buffer_size := arg.MaxRowBufferSize + if max_row_buffer_size == 0 { + max_row_buffer_size = 5 * 1024 * 1024 } + rate := arg.OpsPerSecond + cpu_limit := arg.CpuLimit + iops_limit := arg.IopsLimit + timeout := arg.Timeout if timeout == 0 { timeout = 600 @@ -92,7 +124,7 @@ func (self VQLClientAction) StartQuery( // Cancel the query after this deadline deadline := time.After(time.Second * time.Duration(timeout)) - started := time.Now().Unix() + started := utils.Now().Unix() sub_ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -101,9 +133,11 @@ func (self VQLClientAction) StartQuery( return } + name := strings.Split(utils.GetQueryName(arg.Query), "/")[0] + // Clients do not have a copy of artifacts so they need to be // sent all artifacts from the server. - manager, err := services.GetRepositoryManager() + manager, err := services.GetRepositoryManager(config_obj) if err != nil { responder.RaiseError(ctx, fmt.Sprintf("%v", err)) return @@ -112,7 +146,10 @@ func (self VQLClientAction) StartQuery( repository := manager.NewRepository() for _, artifact := range arg.Artifacts { artifact.BuiltIn = false - _, err := repository.LoadProto(artifact, true /* validate */) + _, err := repository.LoadProto(artifact, + services.ArtifactOptions{ + ValidateArtifact: true, + }) if err != nil { responder.RaiseError(ctx, fmt.Sprintf( "Failed to compile artifact %v.", artifact.Name)) @@ -120,20 +157,30 @@ func (self VQLClientAction) StartQuery( } } - uploader := &uploads.VelociraptorUploader{ - Responder: responder, - } + logger := log.New(NewLogWriter(ctx, config_obj, responder), "", 0) + uploader := uploads.NewVelociraptorUploader(sub_ctx, logger, + time.Duration(timeout)*time.Second, responder) + defer uploader.Close() builder := services.ScopeBuilder{ + Config: &config_proto.Config{ + Client: config_obj.Client, + Remappings: config_obj.Remappings, + }, + Ctx: ctx, + // Only provide the client config since we are running in // client context. ClientConfig: config_obj.Client, // Disable ACLs on the client. - ACLManager: vql_subsystem.NullACLManager{}, - Env: ordereddict.NewDict(), + ACLManager: acl_managers.NullACLManager{}, + Env: ordereddict.NewDict(). + // Make the session id available in the query. + Set("_SessionId", responder.FlowContext().SessionId()). + Set(constants.SCOPE_RESPONDER, responder), Uploader: uploader, Repository: repository, - Logger: log.New(&LogWriter{config_obj, responder, ctx}, "vql: ", 0), + Logger: logger, } for _, env_spec := range arg.Env { @@ -143,6 +190,17 @@ func (self VQLClientAction) StartQuery( scope := manager.BuildScope(builder) defer scope.Close() + // The uploader needs to be flushed before the scope is destroyed + // because transactions may still be active. + defer uploader.Close() + + // Allow VQL to gain access to the flow responder for low level + // functionality. + scope.SetContext(constants.SCOPE_RESPONDER_CONTEXT, responder) + + // Add some additional context for debugging + scope.SetContext(constants.SCOPE_QUERY_NAME, name) + if runtime.GOARCH == "386" && os.Getenv("PROCESSOR_ARCHITEW6432") == "AMD64" { scope.Log("You are running a 32 bit built binary on Windows x64. " + @@ -150,11 +208,22 @@ func (self VQLClientAction) StartQuery( "incorrect or missed results or even crashes.") } - scope.Log("Starting query execution.") + scope.Log("INFO:Starting query execution for %v.", name) + + // Make a throttler + throttler, closer := throttler.NewThrottler(ctx, scope, config_obj, + float64(rate), float64(cpu_limit), float64(iops_limit)) + defer closer() - vfilter.InstallThrottler(scope, vfilter.NewTimeThrottler(float64(rate))) + if arg.ProgressTimeout > 0 { + duration := time.Duration(arg.ProgressTimeout) * time.Second + throttler = NewProgressThrottler( + sub_ctx, scope, cancel, throttler, duration) + scope.Log("query: Installing a progress alarm for %v", duration) + } + scope.SetThrottler(throttler) - start := time.Now() + start := utils.Now() // If we panic we need to recover and report this to the // server. @@ -166,28 +235,31 @@ func (self VQLClientAction) StartQuery( responder.RaiseError(ctx, msg) } - scope.Log("Collection is done after %v", time.Since(start)) + scope.Log("INFO:Collection %v is done after %v", name, time.Since(start)) }() ok, err := CheckPreconditions(ctx, scope, arg) if err != nil { - scope.Log("While evaluating preconditions: %v", err) - responder.RaiseError(ctx, fmt.Sprintf("While evaluating preconditions: %v", err)) + scope.Log("%v: While evaluating preconditions: %v", name, err) + responder.RaiseError(ctx, + fmt.Sprintf("While evaluating preconditions: %v", err)) return } if !ok { - scope.Log("Skipping query due to preconditions") + scope.Log("INFO:%v: Skipping query due to preconditions", name) responder.Return(ctx) return } + row_tracker := NewQueryTracker() + // All the queries will use the same scope. This allows one // query to define functions for the next query in order. for query_idx, query := range arg.Query { query_log := QueryLog.AddQuery(query.VQL) - query_start := uint64(time.Now().UTC().UnixNano() / 1000) + query_start := uint64(utils.Now().UTC().UnixNano() / 1000) vql, err := vfilter.Parse(query.VQL) if err != nil { responder.RaiseError(ctx, err.Error()) @@ -195,17 +267,15 @@ func (self VQLClientAction) StartQuery( return } - result_chan := vfilter.GetResponseChannel( + result_chan := EncodeIntoResponsePackets( vql, sub_ctx, scope, - vql_subsystem.MarshalJsonl(scope), - int(max_row), - int(max_wait)) + int(max_row), int(max_wait), int(max_row_buffer_size)) run_query: for { select { case <-deadline: msg := fmt.Sprintf("Query timed out after %v seconds", - time.Now().Unix()-started) + utils.Now().Unix()-started) scope.Log(msg) // Queries that time out are an error on the server. @@ -217,57 +287,75 @@ func (self VQLClientAction) StartQuery( // can at least return any data it // has. cancel() + uploader.Abort() + scope.Close() // Try again after a while to prevent spinning here. deadline = time.After(time.Second * time.Duration(timeout)) case <-time.After(time.Second * time.Duration(heartbeat)): - responder.Log(ctx, "Time %v: %s: Waiting for rows.", - (uint64(time.Now().UTC().UnixNano()/1000)- - query_start)/1000000, query.Name) + responder.Log(ctx, logging.DEFAULT, + fmt.Sprintf("%v: Time %v: %s: Waiting for rows.", name, + (uint64(utils.Now().UTC().UnixNano()/1000)- + query_start)/1000000, query.Name)) case result, ok := <-result_chan: if !ok { query_log.Close() break run_query } - // Skip let queries since they never produce results. - if strings.HasPrefix(strings.ToLower(query.VQL), "let") { - continue - } + response := &actions_proto.VQLResponse{ Query: query, QueryId: uint64(query_idx), Part: uint64(result.Part), JSONLResponse: string(result.Payload), TotalRows: uint64(result.TotalRows), - Timestamp: uint64(time.Now().UTC().UnixNano() / 1000), + QueryStartRow: row_tracker.GetStartRow(query), + Timestamp: uint64(utils.Now().UTC().UnixNano() / 1000), } + // Do not send empty responses + if result.TotalRows == 0 { + continue + } + + row_tracker.AddRows(query, uint64(result.TotalRows)) + // Don't log empty VQL statements. if query.Name != "" { responder.Log(ctx, - "Time %v: %s: Sending response part %d %s (%d rows).", - (response.Timestamp-query_start)/1000000, - query.Name, - result.Part, - humanize.Bytes(uint64(len(result.Payload))), - result.TotalRows, - ) + logging.DEFAULT, + fmt.Sprintf( + "%v: Time %v: %s: Sending response part %d %s (%d rows).", + name, + (response.Timestamp-query_start)/1000000, + query.Name, + result.Part, + humanize.Bytes(uint64(len(result.Payload))), + result.TotalRows, + )) } response.Columns = result.Columns - responder.AddResponse(ctx, &crypto_proto.VeloMessage{ + responder.AddResponse(&crypto_proto.VeloMessage{ VQLResponse: response}) } } } - if uploader.Count > 0 { - responder.Log(ctx, "Uploaded %v files.", uploader.Count) + if uploader.GetCount() > 0 { + if uploader.GetTransactionCount() > 0 { + responder.Log(ctx, logging.DEFAULT, + fmt.Sprintf("%v: Uploaded %v files with %v outstanding upload transactions.", + name, uploader.GetCount(), + uploader.GetTransactionCount())) + } else { + responder.Log(ctx, logging.DEFAULT, + fmt.Sprintf("%v: Uploaded %v files.", + name, uploader.GetCount())) + } } - - responder.Return(ctx) } func CheckPreconditions( @@ -292,3 +380,102 @@ func CheckPreconditions( } return false, nil } + +func EncodeIntoResponsePackets( + vql *vfilter.VQL, + ctx context.Context, + scope types.Scope, + maxrows int, + // Max time to wait before returning some results. + max_wait int, + // How large do we allow the payload to get + max_row_buffer_size int) <-chan *vfilter.VFilterJsonResult { + result_chan := make(chan *vfilter.VFilterJsonResult) + + encoder := vql_subsystem.MarshalJsonl(scope) + + go func() { + defer close(result_chan) + + part := 0 + row_chan := vql.Eval(ctx, scope) + buffer := bytes.Buffer{} + var columns []string + var total_rows int + + ship_payload := func() { + result := &vfilter.VFilterJsonResult{ + Part: part, + TotalRows: total_rows, + Payload: buffer.Bytes(), + } + + total_rows = 0 + // Use a NEW buffer here to avoid trashing the byte slice + // above. See + // https://github.com/Velocidex/velociraptor/issues/1793 + buffer = bytes.Buffer{} + + result.Columns = columns + result_chan <- result + part += 1 + } + + // Send the last payload outstanding. + defer ship_payload() + + // First deadline is max_wait in the future + deadline := time.After(time.Duration(max_wait) * time.Second) + + for { + select { + case <-ctx.Done(): + return + + // If the query takes too long, send what we + // have. + case <-deadline: + if total_rows > 0 { + ship_payload() + } + + // Update the deadline to re-fire next. + deadline = time.After(time.Duration(max_wait) * time.Second) + + case row, ok := <-row_chan: + if !ok { + return + } + + // Materialize all elements if needed. + value := vfilter.RowToDict(ctx, scope, row) + + // Set the columns according to the first row. + if len(columns) == 0 { + columns = value.Keys() + } + + // Encode the row into bytes ASAP so we can reclaim + // memory. + s, err := encoder([]types.Row{value}) + if err != nil { + scope.Log("Unable to serialize: %v", err) + return + } + // Accumulate the jsonl into the buffer + total_rows++ + buffer.Write(s) + + // Send the payload if it is too full. + if total_rows >= maxrows || + buffer.Len() > max_row_buffer_size { + ship_payload() + deadline = time.After( + time.Duration(max_wait) * time.Second) + } + } + } + }() + + return result_chan +} diff --git a/actions/vql_test.go b/actions/vql_test.go index aa54464f7..f7478b4bf 100644 --- a/actions/vql_test.go +++ b/actions/vql_test.go @@ -1,24 +1,79 @@ package actions_test import ( + "strings" "testing" + "time" - "github.com/alecthomas/assert" "github.com/stretchr/testify/suite" "www.velocidex.com/golang/velociraptor/actions" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" + crypto_proto "www.velocidex.com/golang/velociraptor/crypto/proto" "www.velocidex.com/golang/velociraptor/file_store/test_utils" + "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/responder" + "www.velocidex.com/golang/velociraptor/vtesting" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + + // For execve and query + _ "www.velocidex.com/golang/velociraptor/vql/common" + _ "www.velocidex.com/golang/velociraptor/vql/protocols" + _ "www.velocidex.com/golang/velociraptor/vql/tools" ) type ClientVQLTestSuite struct { test_utils.TestSuite } +func (self *ClientVQLTestSuite) SetupTest() { + self.ConfigObj = self.LoadConfig() + self.ConfigObj.Client.PreventExecve = true + self.TestSuite.SetupTest() +} + +func (self *ClientVQLTestSuite) TestCPUThrottler() { + request := &actions_proto.VQLCollectorArgs{ + Query: []*actions_proto.VQLRequest{ + { + Name: "Query", + VQL: "SELECT 'Boo' FROM scope()", + }, + }, + } + + // Query is not limited + resp := responder.TestResponderWithFlowId( + self.ConfigObj, "TestCPUThrottler") + actions.VQLClientAction{}.StartQuery( + self.ConfigObj, self.Sm.Ctx, resp, request) + resp.Close() + + assert.NotContains(self.T(), getLogs(resp.Drain.Messages()), + "Will throttle query") + + // Query will now be limited + resp = responder.TestResponderWithFlowId( + self.ConfigObj, "TestCPUThrottler2") + defer resp.Close() + + request.CpuLimit = 20 + actions.VQLClientAction{}.StartQuery( + self.ConfigObj, self.Sm.Ctx, resp, request) + + var responses []*crypto_proto.VeloMessage + vtesting.WaitUntil(5*time.Second, self.T(), func() bool { + responses = resp.Drain.Messages() + return strings.Contains(getLogs(responses), "Will throttle query") + }) + + assert.Contains(self.T(), getLogs(responses), "Will throttle query") +} + // Make sure that dependent artifacts are properly used func (self *ClientVQLTestSuite) TestDependentArtifacts() { - resp := responder.TestResponder() + resp := responder.TestResponderWithFlowId( + self.ConfigObj, "TestDependentArtifacts") actions.VQLClientAction{}.StartQuery(self.ConfigObj, self.Sm.Ctx, resp, &actions_proto.VQLCollectorArgs{ @@ -56,20 +111,118 @@ func (self *ClientVQLTestSuite) TestDependentArtifacts() { }, }) - assert.Equal(self.T(), "{\"X\":1,\"_Source\":\"Custom.Foo.Bar.Baz.A\"}\n", getVQLResponse(resp)) + var responses []*crypto_proto.VeloMessage + vtesting.WaitUntil(5*time.Second, self.T(), func() bool { + responses = resp.Drain.Messages() + return "Target: Query, JSONL: {\"X\":1,\"_Source\":\"Custom.Foo.Bar.Baz.A\"}\n\n" == + getVQLResponse(responses) + }) } -func getVQLResponse(resp *responder.Responder) string { - responses := responder.GetTestResponses(resp) - for _, item := range responses { - if item.VQLResponse != nil { - return item.VQLResponse.JSONLResponse - } - } +func (self *ClientVQLTestSuite) TestMaxRows() { + resp := responder.TestResponderWithFlowId(self.ConfigObj, "TestMaxRows") + + actions.VQLClientAction{}.StartQuery(self.ConfigObj, self.Sm.Ctx, resp, + &actions_proto.VQLCollectorArgs{ + MaxRow: 10, + Query: []*actions_proto.VQLRequest{ + { + Name: "Query", + VQL: "SELECT * FROM range(end=20)", + }, + }, + }) + + var responses []*crypto_proto.VeloMessage + vtesting.WaitUntil(time.Second, self.T(), func() bool { + responses = resp.Drain.Messages() + payloads := getResponsePacketCounts(responses) + return len(payloads) == 2 && payloads[0] == 10 && payloads[1] == 10 + }) +} + +func (self *ClientVQLTestSuite) TestExecve() { + resp := responder.TestResponderWithFlowId(self.ConfigObj, "TestMaxRows") + + logging.ClearMemoryLogs() + + actions.VQLClientAction{}.StartQuery(self.ConfigObj, self.Sm.Ctx, resp, + &actions_proto.VQLCollectorArgs{ + MaxRow: 10, + Query: []*actions_proto.VQLRequest{ + { + Name: "Query", + VQL: "SELECT * FROM execve(argv='ls')", + // VQL: "SELECT * FROM query(query={ SELECT * FROM execve(argv='ls') })", + }, + }, + }) + + vtesting.WaitUntil(time.Second, self.T(), func() bool { + return vtesting.MemoryLogsContainRegex( + "execve: Not allowed to execve by configuration.") + }) + + logging.ClearMemoryLogs() + + // Make sure the query() plugin propagates the execve flag + actions.VQLClientAction{}.StartQuery(self.ConfigObj, self.Sm.Ctx, resp, + &actions_proto.VQLCollectorArgs{ + MaxRow: 10, + Query: []*actions_proto.VQLRequest{ + { + Name: "Query", + VQL: "SELECT * FROM query(query={ SELECT * FROM execve(argv='ls') })", + }, + }, + }) - return "" + vtesting.WaitUntil(time.Second, self.T(), func() bool { + return vtesting.MemoryLogsContainRegex( + "execve: Not allowed to execve by configuration.") + }) + +} + +func (self *ClientVQLTestSuite) TestMaxWait() { + assert.True(self.T(), test_utils.Retry(self.T(), 5, time.Millisecond*500, + func(r *test_utils.R) { + resp := responder.TestResponderWithFlowId(self.ConfigObj, "TestMaxWait") + + actions.VQLClientAction{}.StartQuery(self.ConfigObj, self.Sm.Ctx, resp, + &actions_proto.VQLCollectorArgs{ + MaxRow: 1000, + MaxWait: 1, + Query: []*actions_proto.VQLRequest{ + { + Name: "Query", + VQL: "SELECT sleep(ms=400) FROM range(end=4)", + }, + }, + }) + + var responses []*crypto_proto.VeloMessage + + vtesting.WaitUntil(5*time.Second, r, func() bool { + responses = resp.Drain.Messages() + payloads := getResponsePacketCounts(responses) + // Message will be split into 2 packets 2 messages in each + return len(payloads) == 2 && payloads[0] == 2 && payloads[1] == 2 + }) + })) } func TestClientVQL(t *testing.T) { suite.Run(t, &ClientVQLTestSuite{}) } + +func getResponsePacketCounts(responses []*crypto_proto.VeloMessage) []uint64 { + result := []uint64{} + for _, item := range responses { + if item.VQLResponse != nil { + result = append(result, item.VQLResponse.TotalRows) + } + } + + return result +} diff --git a/api/api.go b/api/api.go index 2586dd515..a33c7e760 100644 --- a/api/api.go +++ b/api/api.go @@ -1,23 +1,24 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package api import ( + "context" "crypto/tls" "crypto/x509" "fmt" @@ -29,147 +30,81 @@ import ( "sync" "time" - errors "github.com/pkg/errors" - "github.com/Velocidex/ordereddict" + errors "github.com/go-errors/errors" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/sirupsen/logrus" - context "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/emptypb" "www.velocidex.com/golang/velociraptor/acls" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" - "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/api/authenticators" api_proto "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/api/tables" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" - crypto_utils "www.velocidex.com/golang/velociraptor/crypto/utils" - "www.velocidex.com/golang/velociraptor/flows" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/file_store/path_specs" flows_proto "www.velocidex.com/golang/velociraptor/flows/proto" "www.velocidex.com/golang/velociraptor/grpc_client" "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/paths" - "www.velocidex.com/golang/velociraptor/search" "www.velocidex.com/golang/velociraptor/server" "www.velocidex.com/golang/velociraptor/services" - users "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/utils" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" "www.velocidex.com/golang/vfilter" ) type ApiServer struct { - proto.UnimplementedAPIServer - config *config_proto.Config - server_obj *server.Server - ca_pool *x509.CertPool - + api_proto.UnimplementedAPIServer + server_obj *server.Server + ca_pool *x509.CertPool + wg *sync.WaitGroup + verbose bool api_client_factory grpc_client.APIClientFactory } -func (self *ApiServer) CancelFlow( - ctx context.Context, - in *api_proto.ApiFlowRequest) (*api_proto.StartFlowResponse, error) { - - defer Instrument("CancelFlow")() - - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - - permissions := acls.COLLECT_CLIENT - if in.ClientId == "server" { - permissions = acls.COLLECT_SERVER - } - - perm, err := acls.CheckAccess(self.config, user_name, permissions) - if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, - "User is not allowed to cancel flows.") - } - - result, err := flows.CancelFlow( - ctx, self.config, in.ClientId, in.FlowId, user_name) - if err != nil { - return nil, err - } - - // Log this event as and Audit event. - logging.GetLogger(self.config, &logging.Audit). - WithFields(logrus.Fields{ - "user": user_name, - "client": in.ClientId, - "flow_id": in.FlowId, - "details": fmt.Sprintf("%v", in), - }).Info("CancelFlow") - - return result, nil -} - -func (self *ApiServer) ArchiveFlow( - ctx context.Context, - in *api_proto.ApiFlowRequest) (*api_proto.StartFlowResponse, error) { - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - - defer Instrument("ArchiveFlow")() - - permissions := acls.COLLECT_CLIENT - if in.ClientId == "server" { - permissions = acls.COLLECT_SERVER - } - - perm, err := acls.CheckAccess(self.config, user_name, permissions) - if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, - "User is not allowed to archive flows.") - } - - result, err := flows.ArchiveFlow(self.config, in.ClientId, in.FlowId, user_name) - if err != nil { - return nil, err - } - - // Log this event as and Audit event. - logging.GetLogger(self.config, &logging.Audit). - WithFields(logrus.Fields{ - "user": user_name, - "client": in.ClientId, - "flow_id": in.FlowId, - "details": fmt.Sprintf("%v", in), - }).Info("ArchiveFlow") - - return result, nil -} - func (self *ApiServer) GetReport( ctx context.Context, in *api_proto.GetReportRequest) (*api_proto.GetReportResponse, error) { defer Instrument("GetReport")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view reports.") } - acl_manager := vql_subsystem.NewServerACLManager(self.config, user_name) + acl_manager := acl_managers.NewServerACLManager(org_config_obj, principal) - manager, err := services.GetRepositoryManager() + manager, err := services.GetRepositoryManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - global_repo, err := manager.GetGlobalRepository(self.config) + global_repo, err := manager.GetGlobalRepository(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - return getReport(ctx, self.config, acl_manager, global_repo, in) + return getReport(ctx, org_config_obj, acl_manager, global_repo, in) } func (self *ApiServer) CollectArtifact( @@ -179,61 +114,67 @@ func (self *ApiServer) CollectArtifact( defer Instrument("CollectArtifact")() result := &flows_proto.ArtifactCollectorResponse{Request: in} - creator := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - - var acl_manager vql_subsystem.ACLManager = vql_subsystem.NullACLManager{} - - // Internal calls from the frontend can set the creator. - if creator != self.config.Client.PinnedServerName { - in.Creator = creator - permissions := acls.COLLECT_CLIENT - if in.ClientId == "server" { - permissions = acls.COLLECT_SERVER - } - - acl_manager = vql_subsystem.NewServerACLManager(self.config, - creator) - - perm, err := acl_manager.CheckAccess(permissions) - if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, - "User is not allowed to launch flows.") - } - } - - manager, err := services.GetRepositoryManager() + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + // Build a request based on user input. + request := &flows_proto.ArtifactCollectorArgs{ + ClientId: in.ClientId, + Artifacts: in.Artifacts, + Specs: in.Specs, + Creator: user_record.Name, + OpsPerSecond: in.OpsPerSecond, + CpuLimit: in.CpuLimit, + IopsLimit: in.IopsLimit, + Timeout: in.Timeout, + ProgressTimeout: in.ProgressTimeout, + MaxRows: in.MaxRows, + MaxLogs: in.MaxLogs, + MaxUploadBytes: in.MaxUploadBytes, + Urgent: in.Urgent, + TraceFreqSec: in.TraceFreqSec, + } + + acl_manager := acl_managers.NewServerACLManager( + org_config_obj, user_record.Name) + + manager, err := services.GetRepositoryManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - repository, err := manager.GetGlobalRepository(self.config) + repository, err := manager.GetGlobalRepository(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - launcher, err := services.GetLauncher() + + launcher, err := services.GetLauncher(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } flow_id, err := launcher.ScheduleArtifactCollection( - ctx, self.config, acl_manager, repository, in, nil) + ctx, org_config_obj, acl_manager, repository, request, + utils.BackgroundWriter) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } result.FlowId = flow_id // Log this event as an Audit event. - logging.GetLogger(self.config, &logging.Audit). - WithFields(logrus.Fields{ - "user": in.Creator, - "client": in.ClientId, - "flow_id": flow_id, - "details": fmt.Sprintf("%v", in), - }).Info("CollectArtifact") + err = services.LogAudit(ctx, + org_config_obj, request.Creator, "ScheduleFlow", + ordereddict.NewDict(). + Set("client", request.ClientId). + Set("flow_id", flow_id). + Set("details", request)) - return result, nil + return result, err } func (self *ApiServer) ListClients( @@ -242,26 +183,39 @@ func (self *ApiServer) ListClients( defer Instrument("ListClients")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view clients.") } - result, err := search.SearchClients(ctx, self.config, in, user_name) + indexer, err := services.GetIndexer(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + result, err := indexer.SearchClients(ctx, org_config_obj, in, principal) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } // Warm up the cache pre-emptively so we have fresh connected // status - notifier := services.GetNotifier() + notifier, err := services.GetNotifier(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } for _, item := range result.Items { notifier.IsClientConnected( - ctx, self.config, item.ClientId, 0 /* timeout */) + ctx, org_config_obj, item.ClientId, 0 /* timeout */) } return result, nil } @@ -272,28 +226,34 @@ func (self *ApiServer) NotifyClients( defer Instrument("NotifyClients")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.COLLECT_CLIENT - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to launch flows.") } - notifier := services.GetNotifier() - if notifier == nil { - return nil, errors.New("Notifier not ready") + notifier, err := services.GetNotifier(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) } if in.ClientId != "" { self.server_obj.Info("sending notification to %s", in.ClientId) - err = services.GetNotifier().NotifyListener( - self.config, in.ClientId, "API.NotifyClients") + err = notifier.NotifyListener(ctx, org_config_obj, in.ClientId, + "API.NotifyClients") } else { return nil, status.Error(codes.InvalidArgument, "client id should be specified") } - return &emptypb.Empty{}, err + return &emptypb.Empty{}, Status(self.verbose, err) } func (self *ApiServer) LabelClients( @@ -302,9 +262,14 @@ func (self *ApiServer) LabelClients( defer Instrument("LabelClients")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name permissions := acls.LABEL_CLIENT - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { return &api_proto.APIResponse{ Error: true, @@ -313,15 +278,37 @@ func (self *ApiServer) LabelClients( "User is not allowed to label clients.") } - labeler := services.GetLabeler() + labeler := services.GetLabeler(org_config_obj) for _, client_id := range in.ClientIds { for _, label := range in.Labels { switch in.Operation { case "set": - err = labeler.SetClientLabel(self.config, client_id, label) + err = labeler.SetClientLabel(ctx, + org_config_obj, client_id, label) + if err == nil { + err := services.LogAudit(ctx, + org_config_obj, principal, "SetClientLabel", + ordereddict.NewDict(). + Set("client_id", client_id). + Set("label", label)) + if err != nil { + return nil, Status(self.verbose, err) + } + } case "remove": - err = labeler.RemoveClientLabel(self.config, client_id, label) + err = labeler.RemoveClientLabel(ctx, + org_config_obj, client_id, label) + if err == nil { + err := services.LogAudit(ctx, + org_config_obj, principal, "RemoveClientLabel", + ordereddict.NewDict(). + Set("client_id", client_id). + Set("label", label)) + if err != nil { + return nil, Status(self.verbose, err) + } + } default: return nil, errors.New("Unknown label operation") @@ -331,7 +318,7 @@ func (self *ApiServer) LabelClients( return &api_proto.APIResponse{ Error: true, ErrorMessage: err.Error(), - }, err + }, Status(self.verbose, err) } } } @@ -345,16 +332,31 @@ func (self *ApiServer) GetFlowDetails( defer Instrument("GetFlowDetails")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to launch flows.") } - result, err := flows.GetFlowDetails(self.config, in.ClientId, in.FlowId) - return result, err + launcher, err := services.GetLauncher(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + result, err := launcher.GetFlowDetails( + ctx, org_config_obj, services.GetFlowOptions{Downloads: true}, + in.ClientId, in.FlowId) + if err != nil { + return nil, Status(self.verbose, err) + } + return result, nil } func (self *ApiServer) GetFlowRequests( @@ -363,38 +365,77 @@ func (self *ApiServer) GetFlowRequests( defer Instrument("GetFlowRequests")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view flows.") } - result, err := flows.GetFlowRequests(self.config, in.ClientId, in.FlowId, - in.Offset, in.Count) - return result, err + launcher, err := services.GetLauncher(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + result, err := launcher.Storage().GetFlowRequests( + ctx, org_config_obj, in.ClientId, in.FlowId, in.Offset, in.Count) + return result, Status(self.verbose, err) } func (self *ApiServer) GetUserUITraits( ctx context.Context, - in *emptypb.Empty) (*api_proto.ApiGrrUser, error) { - result := NewDefaultUserObject(self.config) - user_info := GetGRPCUserInfo(self.config, ctx, self.ca_pool) - + in *emptypb.Empty) (*api_proto.ApiUser, error) { defer Instrument("GetUserUITraits")() - result.Username = user_info.Name - result.InterfaceTraits.Picture = user_info.Picture - result.InterfaceTraits.Permissions, _ = acls.GetEffectivePolicy(self.config, + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + authenticator, err := authenticators.NewAuthenticator(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + result := NewDefaultUserObject(org_config_obj) + result.Username = principal + result.InterfaceTraits.PasswordLess = authenticator.IsPasswordLess() + result.InterfaceTraits.AuthRedirectTemplate = authenticator.AuthRedirectTemplate() + result.InterfaceTraits.Picture = user_record.Picture + result.InterfaceTraits.Permissions, _ = services.GetEffectivePolicy(org_config_obj, result.Username) + result.Orgs = user_record.Orgs - user_options, err := users.GetUserOptions(self.config, result.Username) + for _, item := range result.Orgs { + if utils.IsRootOrg(item.Id) { + item.Name = services.ROOT_ORG_NAME + item.Id = services.ROOT_ORG_ID + } + } + + user_options, err := users.GetUserOptions(ctx, result.Username) if err == nil { + result.InterfaceTraits.Org = org_config_obj.OrgId + result.InterfaceTraits.OrgName = org_config_obj.OrgName result.InterfaceTraits.UiSettings = user_options.Options result.InterfaceTraits.Theme = user_options.Theme + result.InterfaceTraits.Timezone = user_options.Timezone + result.InterfaceTraits.Lang = user_options.Lang result.InterfaceTraits.DefaultPassword = user_options.DefaultPassword result.InterfaceTraits.DefaultDownloadsLock = user_options.DefaultDownloadsLock + result.InterfaceTraits.Customizations = user_options.Customizations + result.InterfaceTraits.Links = user_options.Links + result.InterfaceTraits.DisableServerEvents = user_options.DisableServerEvents + result.InterfaceTraits.DisableQuarantineButton = user_options.DisableQuarantineButton + result.Messages = user_options.Messages } return result, nil @@ -402,30 +443,51 @@ func (self *ApiServer) GetUserUITraits( func (self *ApiServer) SetGUIOptions( ctx context.Context, - in *api_proto.SetGUIOptionsRequest) (*emptypb.Empty, error) { - user_info := GetGRPCUserInfo(self.config, ctx, self.ca_pool) + in *api_proto.SetGUIOptionsRequest) (*api_proto.SetGUIOptionsResponse, error) { defer Instrument("SetGUIOptions")() - return &emptypb.Empty{}, users.SetUserOptions(self.config, user_info.Name, in) + users := services.GetUserManager() + user_record, _, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + // This API is only used for the user to change their own options + // so it is always allowed. + return &api_proto.SetGUIOptionsResponse{}, + users.SetUserOptions(ctx, principal, principal, in) } +// Only list the child directories - used by the tree widget. func (self *ApiServer) VFSListDirectory( ctx context.Context, in *api_proto.VFSListRequest) (*api_proto.VFSListResponse, error) { defer Instrument("VFSListDirectory")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view the VFS.") } - result, err := vfsListDirectory( - self.config, in.ClientId, in.VfsComponents) + vfs_service, err := services.GetVFSService(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + result, err := vfs_service.ListDirectories(ctx, + org_config_obj, in.ClientId, in.VfsComponents) return result, err } @@ -435,17 +497,28 @@ func (self *ApiServer) VFSStatDirectory( defer Instrument("VFSStatDirectory")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to launch flows.") } - result, err := vfsStatDirectory( - self.config, in.ClientId, in.VfsComponents) - return result, err + vfs_service, err := services.GetVFSService(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + result, err := vfs_service.StatDirectory( + org_config_obj, in.ClientId, in.VfsComponents) + return result, Status(self.verbose, err) } func (self *ApiServer) VFSStatDownload( @@ -454,17 +527,28 @@ func (self *ApiServer) VFSStatDownload( defer Instrument("VFSStatDownload")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view the VFS.") } - result, err := vfsStatDownload( - self.config, in.ClientId, in.Accessor, in.Components) - return result, err + vfs_service, err := services.GetVFSService(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + result, err := vfs_service.StatDownload( + org_config_obj, in.ClientId, in.Accessor, in.Components) + return result, Status(self.verbose, err) } func (self *ApiServer) VFSRefreshDirectory( @@ -474,17 +558,23 @@ func (self *ApiServer) VFSRefreshDirectory( defer Instrument("VFSRefreshDirectory")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.COLLECT_CLIENT - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to launch flows.") } result, err := vfsRefreshDirectory( self, ctx, in.ClientId, in.VfsComponents, in.Depth) - return result, err + return result, Status(self.verbose, err) } func (self *ApiServer) VFSGetBuffer( @@ -494,20 +584,59 @@ func (self *ApiServer) VFSGetBuffer( defer Instrument("VFSGetBuffer")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + // The user may request to download a buffer from any org. + if !utils.CompareOrgIds(org_config_obj.OrgId, in.OrgId) { + org_manager, err := services.GetOrgManager() + if err != nil { + return nil, Status(self.verbose, err) + } + + org_config_obj, err = org_manager.GetOrgConfig(in.OrgId) + if err != nil { + return nil, Status(self.verbose, err) + } + } + + principal := user_record.Name + + // Make sure the principal has permission in the org. permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view the VFS.") } - path_spec := paths.NewClientPathManager( - in.ClientId).FSItem(in.Components) - result, err := vfsGetBuffer( - self.config, in.ClientId, path_spec, in.Offset, in.Length) + // If a client id is specified, the path is relative to the + // client's storage directory, otherwise it is relative to the + // root of the filestore. + var pathspec api.FSPathSpec + if in.ClientId != "" { + pathspec = paths.NewClientPathManager( + in.ClientId).FSItem(in.Components) - return result, err + } else if len(in.Components) > 0 { + pathspec = path_specs.FromGenericComponentList(in.Components) + + } else { + return nil, status.Error(codes.InvalidArgument, + "Invalid pathspec") + } + + padding := true + if in.Padding != nil { + padding = *in.Padding + } + + result, err := vfsGetBuffer(org_config_obj, in.ClientId, pathspec, in.Offset, in.Length, padding) + + return result, Status(self.verbose, err) } func (self *ApiServer) GetTable( @@ -516,50 +645,25 @@ func (self *ApiServer) GetTable( defer Instrument("GetTable")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view results.") } - var result *api_proto.GetTableResponse - - // We want an event table. - if in.Type == "TIMELINE" { - result, err = getTimeline(ctx, self.config, in) - - } else if in.Type == "CLIENT_EVENT_LOGS" || in.Type == "SERVER_EVENT_LOGS" { - result, err = getEventTableLogs(ctx, self.config, in) - - } else if in.Type == "CLIENT_EVENT" || in.Type == "SERVER_EVENT" { - result, err = getEventTable(ctx, self.config, in) - - } else { - result, err = getTable(ctx, self.config, in) - } - + result, err := tables.GetTable(ctx, org_config_obj, in, principal) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - if in.Artifact != "" { - manager, err := services.GetRepositoryManager() - if err != nil { - return nil, err - } - - repository, err := manager.GetGlobalRepository(self.config) - if err != nil { - return nil, err - } - - artifact, pres := repository.Get(self.config, in.Artifact) - if pres { - result.ColumnTypes = artifact.ColumnTypes - } - } return result, nil } @@ -570,30 +674,46 @@ func (self *ApiServer) GetArtifacts( defer Instrument("GetArtifacts")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view custom artifacts.") } if len(in.Names) > 0 { result := &artifacts_proto.ArtifactDescriptors{} - manager, err := services.GetRepositoryManager() + manager, err := services.GetRepositoryManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - repository, err := manager.GetGlobalRepository(self.config) + repository, err := manager.GetGlobalRepository(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } for _, name := range in.Names { - artifact, pres := repository.Get(self.config, name) + artifact, pres := repository.Get(ctx, org_config_obj, name) + if !pres { + continue + } + + artifact_clone := proto.Clone(artifact).(*artifacts_proto.Artifact) + for _, s := range artifact_clone.Sources { + s.Queries = nil + } + artifact_clone.Raw = "" + if pres { - result.Items = append(result.Items, artifact) + result.Items = append(result.Items, artifact_clone) } } return result, nil @@ -601,13 +721,13 @@ func (self *ApiServer) GetArtifacts( if in.ReportType != "" { return getReportArtifacts( - self.config, in.ReportType, in.NumberOfResults) + ctx, org_config_obj, in.ReportType, in.NumberOfResults) } - terms := strings.Split(in.SearchTerm, " ") result, err := searchArtifact( - self.config, terms, in.Type, in.NumberOfResults, in.Fields) - return result, err + ctx, org_config_obj, in.SearchTerm, + in.Type, in.NumberOfResults, in.Fields) + return result, Status(self.verbose, err) } func (self *ApiServer) GetArtifactFile( @@ -617,17 +737,23 @@ func (self *ApiServer) GetArtifactFile( defer Instrument("GetArtifactFile")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view custom artifacts.") } - artifact, err := getArtifactFile(self.config, in.Name) + artifact, err := getArtifactFile(ctx, org_config_obj, in.Name) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } result := &api_proto.GetArtifactResponse{ @@ -638,112 +764,149 @@ func (self *ApiServer) GetArtifactFile( func (self *ApiServer) SetArtifactFile( ctx context.Context, - in *api_proto.SetArtifactRequest) ( - *api_proto.APIResponse, error) { + in *api_proto.SetArtifactRequest) (*api_proto.SetArtifactResponse, error) { defer Instrument("SetArtifactFile")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.ARTIFACT_WRITER - // First ensure that the artifact is correct. - manager, err := services.GetRepositoryManager() + // Verify the artifact first, then only set it if there are no + // errors or warnings. + if in.Op == api_proto.SetArtifactRequest_CHECK_AND_SET { + state, err := checkArtifact(ctx, org_config_obj, in.Artifact) + if err != nil { + return nil, Status(self.verbose, err) + } + + // report the errors and warnings + if len(state.Errors) != 0 || len(state.Warnings) != 0 { + res := &api_proto.SetArtifactResponse{ + Error: true, + Warnings: state.Warnings, + } + + for _, e := range state.Errors { + res.Errors = append(res.Errors, e) + } + + return res, nil + } + + // Fallback to regular setting. + in.Op = api_proto.SetArtifactRequest_SET + } + + // We need to load the artifact to figure out what type it is + // first. Depending on the artifact type we need to check the + // relevant permission. + manager, err := services.GetRepositoryManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } tmp_repository := manager.NewRepository() artifact_definition, err := tmp_repository.LoadYaml( - in.Artifact, true /* validate */, false /* built_in */) + in.Artifact, services.ArtifactOptions{ + ValidateArtifact: true, + }) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } switch strings.ToUpper(artifact_definition.Type) { case "CLIENT", "CLIENT_EVENT": permissions = acls.ARTIFACT_WRITER - case "SERVER", "SERVER_EVENT": + case "SERVER", "SERVER_EVENT", "NOTEBOOK": permissions = acls.SERVER_ARTIFACT_WRITER } - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, fmt.Sprintf( - "User is not allowed to modify artifacts (%v).", permissions)) + return nil, PermissionDenied(err, + fmt.Sprintf("User is not allowed to modify artifacts (%v).", permissions)) } - definition, err := setArtifactFile(self.config, user_name, in, "") + definition, err := setArtifactFile(ctx, org_config_obj, principal, in, "") if err != nil { - message := &api_proto.APIResponse{ + message := &api_proto.SetArtifactResponse{ Error: true, ErrorMessage: fmt.Sprintf("%v", err), } - return message, errors.New(message.ErrorMessage) + return message, Status(self.verbose, errors.New(message.ErrorMessage)) } - logging.GetLogger(self.config, &logging.Audit). - WithFields(logrus.Fields{ - "user": user_name, - "artifact": definition.Name, - "details": fmt.Sprintf("%v", in.Artifact), - }).Info("SetArtifactFile") + err = services.LogAudit(ctx, + org_config_obj, principal, "SetArtifactFile", + ordereddict.NewDict(). + Set("artifact", definition.Name). + Set("details", in.Artifact)) - return &api_proto.APIResponse{}, nil + return &api_proto.SetArtifactResponse{}, err } func (self *ApiServer) Query( in *actions_proto.VQLCollectorArgs, stream api_proto.API_QueryServer) error { - // Get the TLS context from the peer and verify its - // certificate. - peer, ok := peer.FromContext(stream.Context()) - if !ok { - return status.Error(codes.InvalidArgument, "cant get peer info") - } + defer Instrument("Query")() - tlsInfo, ok := peer.AuthInfo.(credentials.TLSInfo) - if !ok { - return status.Error(codes.InvalidArgument, "unable to get credentials") + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(stream.Context()) + if err != nil { + return err } + principal := user_record.Name - // Authenticate API clients using certificates. - for _, peer_cert := range tlsInfo.State.PeerCertificates { - chains, err := peer_cert.Verify( - x509.VerifyOptions{Roots: self.ca_pool}) + // If the caller wants to switch orgs, change the config to point + // to that org. We check permission immediately below to ensure + // they actually have the permission to query this org. + if in.OrgId != "" { + // Fetch the appropriate config file fro the org manager. + org_manager, err := services.GetOrgManager() if err != nil { - return err + return Status(self.verbose, err) } - if len(chains) == 0 { - return status.Error(codes.InvalidArgument, "no chains verified") - } - - peer_name := crypto_utils.GetSubjectName(peer_cert) - - // Check that the principal is allowed to issue queries. - permissions := acls.ANY_QUERY - ok, err := acls.CheckAccess(self.config, peer_name, permissions) + org_config_obj, err = org_manager.GetOrgConfig(in.OrgId) if err != nil { - return status.Error(codes.PermissionDenied, - fmt.Sprintf("User %v is not allowed to run queries.", - peer_name)) + return Status(self.verbose, err) } + } - if !ok { - return status.Error(codes.PermissionDenied, fmt.Sprintf( - "Permission denied: User %v requires permission %v to run queries", - peer_name, permissions)) - } + // Check that the principal is allowed to issue queries. + permissions := acls.ANY_QUERY + ok, err := services.CheckAccess(org_config_obj, principal, permissions) + if err != nil { + return status.Error(codes.PermissionDenied, + fmt.Sprintf("User %v is not allowed to run queries.", + principal)) + } - // return the first good match - if true { - // Cert is good enough for us, run the query. - return streamQuery(stream.Context(), self.config, in, stream, peer_name) - } + if !ok { + return status.Error(codes.PermissionDenied, fmt.Sprintf( + "Permission denied: User %v requires permission %v to run queries", + principal, permissions)) + } + + peer, ok := peer.FromContext(stream.Context()) + if !ok { + return status.Error(codes.PermissionDenied, "No peer") } - return status.Error(codes.InvalidArgument, "no peer certs?") + _ = users.SetUserStats(stream.Context(), org_config_obj, principal, + &api_proto.UserStats{ + LastActiveTime: utils.GetTime().Now().Unix(), + LastIpAddress: peer.Addr.String(), + }) + + return streamQuery(stream.Context(), org_config_obj, in, stream, principal) } func (self *ApiServer) GetServerMonitoringState( @@ -753,16 +916,26 @@ func (self *ApiServer) GetServerMonitoringState( defer Instrument("GetServerMonitoringState")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, fmt.Sprintf( - "User is not allowed to read results (%v).", permissions)) + return nil, PermissionDenied(err, + fmt.Sprintf("User is not allowed to read results (%v).", permissions)) } - result, err := getServerMonitoringState(self.config) - return result, err + server_event_manager, err := services.GetServerEventManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + return server_event_manager.Get(), nil } func (self *ApiServer) SetServerMonitoringState( @@ -772,16 +945,29 @@ func (self *ApiServer) SetServerMonitoringState( defer Instrument("SetServerMonitoringState")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - permissions := acls.SERVER_ADMIN - perm, err := acls.CheckAccess(self.config, user_name, permissions) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + // Monitoring queries needs same permissions as regular artifact + // collections. + permissions := acls.COLLECT_SERVER + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, fmt.Sprintf( - "User is not allowed to modify artifacts (%v).", permissions)) + return nil, PermissionDenied(err, + fmt.Sprintf("User is not allowed to modify artifacts (%v).", permissions)) + } + + server_event_manager, err := services.GetServerEventManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) } - err = setServerMonitoringState(self.config, user_name, in) - return in, err + err = server_event_manager.Update(ctx, org_config_obj, principal, in) + return in, Status(self.verbose, err) } func (self *ApiServer) GetClientMonitoringState( @@ -790,23 +976,33 @@ func (self *ApiServer) GetClientMonitoringState( defer Instrument("GetClientMonitoringState")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - permissions := acls.SERVER_ADMIN - perm, err := acls.CheckAccess(self.config, user_name, permissions) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, fmt.Sprintf( - "User is not allowed to read monitoring artifacts (%v).", permissions)) + return nil, PermissionDenied(err, + fmt.Sprintf("User is not allowed to read monitoring artifacts (%v).", permissions)) + } + + manager, err := services.ClientEventManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) } - manager := services.ClientEventManager() result := manager.GetClientMonitoringState() if in.ClientId != "" { - message := manager.GetClientUpdateEventTableMessage(self.config, - in.ClientId) + message := manager.GetClientUpdateEventTableMessage( + ctx, org_config_obj, in.ClientId) result.ClientMessage = message } - return result, err + return result, Status(self.verbose, err) } func (self *ApiServer) SetClientMonitoringState( @@ -816,21 +1012,30 @@ func (self *ApiServer) SetClientMonitoringState( defer Instrument("SetClientMonitoringState")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - permissions := acls.SERVER_ADMIN - perm, err := acls.CheckAccess(self.config, user_name, permissions) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.COLLECT_CLIENT + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, fmt.Sprintf( - "User is not allowed to modify monitoring artifacts (%v).", permissions)) + return nil, PermissionDenied(err, + fmt.Sprintf("User is not allowed to modify monitoring artifacts (%v).", permissions)) + } + + manager, err := services.ClientEventManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) } - err = services.ClientEventManager().SetClientMonitoringState( - ctx, self.config, user_name, in) + err = manager.SetClientMonitoringState(ctx, org_config_obj, principal, in) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - return &emptypb.Empty{}, err + return &emptypb.Empty{}, nil } func (self *ApiServer) CreateDownloadFile(ctx context.Context, @@ -838,68 +1043,83 @@ func (self *ApiServer) CreateDownloadFile(ctx context.Context, defer Instrument("CreateDownloadFile")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.PREPARE_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, fmt.Sprintf( - "User is not allowed to create downloads (%v).", permissions)) + return nil, PermissionDenied(err, + fmt.Sprintf("User is not allowed to create downloads (%v).", permissions)) } // Log an audit event. - logging.GetLogger(self.config, &logging.Audit). - WithFields(logrus.Fields{ - "user": user_name, - "request": in, - }).Info("CreateDownloadRequest") + err = services.LogAudit(ctx, + org_config_obj, principal, "CreateDownloadRequest", + ordereddict.NewDict().Set("request", in)) + if !perm || err != nil { + return nil, PermissionDenied(err, + fmt.Sprintf("User is not allowed to create downloads (%v).", permissions)) + } format := "" - if in.JsonFormat { + if in.JsonFormat && !in.CsvFormat { format = "json" - } else if in.CsvFormat { + } else if in.CsvFormat && !in.JsonFormat { + format = "csv_only" + } else if in.CsvFormat && in.JsonFormat { format = "csv" + } else { + format = "json" } query := "" env := ordereddict.NewDict() if in.FlowId != "" && in.ClientId != "" { - query = `SELECT create_flow_download(password=Password, - client_id=ClientId, flow_id=FlowId, type=DownloadType) AS VFSPath + query = `SELECT create_flow_download(password=Password, format=Format, + expand_sparse=ExpandSparse, client_id=ClientId, flow_id=FlowId) AS VFSPath FROM scope()` env.Set("ClientId", in.ClientId). Set("FlowId", in.FlowId). Set("Password", in.Password). - Set("DownloadType", in.DownloadType) + Set("Format", format). + Set("ExpandSparse", in.ExpandSparse) } else if in.HuntId != "" { query = `SELECT create_hunt_download(password=Password, + expand_sparse=ExpandSparse, hunt_id=HuntId, only_combined=OnlyCombined, format=Format) AS VFSPath FROM scope()` env.Set("HuntId", in.HuntId). Set("Format", format). Set("Password", in.Password). - Set("OnlyCombined", in.OnlyCombinedHunt) + Set("OnlyCombined", in.OnlyCombinedHunt). + Set("ExpandSparse", in.ExpandSparse) } - manager, err := services.GetRepositoryManager() + manager, err := services.GetRepositoryManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } scope := manager.BuildScope( services.ScopeBuilder{ - Config: self.config, + Config: org_config_obj, Env: env, - ACLManager: vql_subsystem.NewServerACLManager(self.config, user_name), - Logger: logging.NewPlainLogger(self.config, &logging.FrontendComponent), + ACLManager: acl_managers.NewServerACLManager(org_config_obj, principal), + Logger: logging.NewPlainLogger(org_config_obj, &logging.FrontendComponent), }) defer scope.Close() vql, err := vfilter.Parse(query) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } sub_ctx, cancel := context.WithCancel(ctx) @@ -910,7 +1130,7 @@ func (self *ApiServer) CreateDownloadFile(ctx context.Context, result.VfsPath = vql_subsystem.GetStringFromRow(scope, row, "VFSPath") } - return result, err + return result, Status(self.verbose, err) } func startAPIServer( @@ -933,7 +1153,7 @@ func startAPIServer( lis, err := net.Listen(config_obj.API.BindScheme, bind_addr) if err != nil { - return errors.WithStack(err) + return errors.Wrap(err, 0) } // Use the server certificate to secure the gRPC connection. @@ -941,29 +1161,38 @@ func startAPIServer( []byte(config_obj.Frontend.Certificate), []byte(config_obj.Frontend.PrivateKey)) if err != nil { - return errors.WithStack(err) + return errors.Wrap(err, 0) } // Authenticate API clients using certificates. CA_Pool := x509.NewCertPool() - CA_Pool.AppendCertsFromPEM([]byte(config_obj.Client.CaCertificate)) + if config_obj.Client != nil { + CA_Pool.AppendCertsFromPEM([]byte(config_obj.Client.CaCertificate)) + } // Create the TLS credentials - creds := credentials.NewTLS(&tls.Config{ - // Only accept certs signed by the CA - ClientAuth: tls.RequireAndVerifyClientCert, - Certificates: []tls.Certificate{cert}, - ClientCAs: CA_Pool, - }) + tls_config := &tls.Config{} + err = getTLSConfig(config_obj, tls_config) + if err != nil { + return err + } + + // Only accept certs signed by the Velociraptor internal CA + tls_config.ClientAuth = tls.RequireAndVerifyClientCert + tls_config.Certificates = []tls.Certificate{cert} + tls_config.ClientCAs = CA_Pool + + creds := credentials.NewTLS(tls_config) grpcServer := grpc.NewServer(grpc.Creds(creds)) api_proto.RegisterAPIServer( grpcServer, &ApiServer{ - config: config_obj, server_obj: server_obj, + verbose: config_obj.Verbose, ca_pool: CA_Pool, api_client_factory: grpc_client.GRPCAPIClient{}, + wg: wg, }, ) // Register reflection service. @@ -1005,7 +1234,7 @@ func StartMonitoringService( logger := logging.GetLogger(config_obj, &logging.FrontendComponent) - env_inject_time, pres := os.LookupEnv("VELOCIRAPTOR_INJECT_API_SLEEP") + env_inject_time, pres := os.LookupEnv(constants.VELOCIRAPTOR_INJECT_API_SLEEP) if pres { logger.Info("Injecting delays for API calls since VELOCIRAPTOR_INJECT_API_SLEEP is set (only used for testing).") result, err := strconv.ParseInt(env_inject_time, 0, 64) @@ -1018,7 +1247,7 @@ func StartMonitoringService( config_obj.Monitoring.BindAddress, config_obj.Monitoring.BindPort) - mux := http.NewServeMux() + mux := api_utils.NewServeMux() mux.Handle("/metrics", promhttp.Handler()) server := &http.Server{ Addr: bind_addr, @@ -1044,8 +1273,9 @@ func StartMonitoringService( <-ctx.Done() logger.Info("Shutting down Prometheus monitoring service") - timeout_ctx, cancel := context.WithTimeout( - context.Background(), 10*time.Second) + timeout_ctx, cancel := utils.WithTimeoutCause( + context.Background(), 10*time.Second, + errors.New("Monitoring Service deadline reached")) defer cancel() err := server.Shutdown(timeout_ctx) diff --git a/api/artifacts.go b/api/artifacts.go index 94be0de99..64a76aae4 100644 --- a/api/artifacts.go +++ b/api/artifacts.go @@ -1,45 +1,46 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package api import ( - "archive/zip" "bytes" - "errors" - "fmt" - "io/ioutil" + "context" "regexp" "strings" - "github.com/sirupsen/logrus" - context "golang.org/x/net/context" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "github.com/Velocidex/ordereddict" + errors "github.com/go-errors/errors" + file_store_accessor "www.velocidex.com/golang/velociraptor/accessors/file_store" "www.velocidex.com/golang/velociraptor/acls" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" api_proto "www.velocidex.com/golang/velociraptor/api/proto" artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/file_store" + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/file_store/path_specs" flows_proto "www.velocidex.com/golang/velociraptor/flows/proto" "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/paths" "www.velocidex.com/golang/velociraptor/services" - users "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services/launcher" + "www.velocidex.com/golang/velociraptor/third_party/zip" "www.velocidex.com/golang/velociraptor/utils" ) @@ -52,7 +53,7 @@ const ( description: | This is the human readable description of the artifact. -# Can be CLIENT, CLIENT_EVENT, SERVER, SERVER_EVENT +# Can be CLIENT, CLIENT_EVENT, SERVER, SERVER_EVENT or NOTEBOOK type: CLIENT parameters: @@ -70,10 +71,10 @@ sources: ) func getArtifactFile( - config_obj *config_proto.Config, + ctx context.Context, config_obj *config_proto.Config, name string) (string, error) { - manager, err := services.GetRepositoryManager() + manager, err := services.GetRepositoryManager(config_obj) if err != nil { return "", err } @@ -83,7 +84,7 @@ func getArtifactFile( return "", err } - artifact, pres := repository.Get(config_obj, name) + artifact, pres := repository.Get(ctx, config_obj, name) if !pres { return default_artifact, nil } @@ -110,14 +111,14 @@ func ensureArtifactPrefix(definition, prefix string) string { }) } -func setArtifactFile(config_obj *config_proto.Config, principal string, - in *api_proto.SetArtifactRequest, - required_prefix string) ( +func setArtifactFile( + ctx context.Context, config_obj *config_proto.Config, principal string, + in *api_proto.SetArtifactRequest, required_prefix string) ( *artifacts_proto.Artifact, error) { - manager, err := services.GetRepositoryManager() + manager, err := services.GetRepositoryManager(config_obj) if err != nil { - return nil, err + return nil, Status(config_obj.Verbose, err) } switch in.Op { @@ -126,29 +127,94 @@ func setArtifactFile(config_obj *config_proto.Config, principal string, // First ensure that the artifact is correct. tmp_repository := manager.NewRepository() artifact_definition, err := tmp_repository.LoadYaml( - in.Artifact, true /* validate */, false /* built_in */) + in.Artifact, services.ArtifactOptions{ + ValidateArtifact: true, + }) if err != nil { - return nil, err + return nil, Status(config_obj.Verbose, err) } if !strings.HasPrefix(artifact_definition.Name, required_prefix) { - return nil, errors.New( + return nil, InvalidStatus( "Modified or custom artifact names must start with '" + required_prefix + "'") } - return artifact_definition, manager.DeleteArtifactFile(config_obj, + return artifact_definition, manager.DeleteArtifactFile(ctx, config_obj, principal, artifact_definition.Name) + case api_proto.SetArtifactRequest_CHECK: + tmp_repository := manager.NewRepository() + return tmp_repository.LoadYaml( + in.Artifact, services.ArtifactOptions{ + ValidateArtifact: true, + }) + case api_proto.SetArtifactRequest_SET: - return manager.SetArtifactFile( + result, err := manager.SetArtifactFile(ctx, config_obj, principal, in.Artifact, required_prefix) + if err != nil { + return nil, Status(config_obj.Verbose, err) + } + + if len(in.Tags) > 0 { + err = manager.SetArtifactMetadata(ctx, config_obj, principal, + result.Name, &artifacts_proto.ArtifactMetadata{ + Tags: in.Tags, + }) + if err != nil { + return nil, Status(config_obj.Verbose, err) + } + } + + return result, nil + } - return nil, errors.New("Unknown op") + return nil, InvalidStatus("Unknown op") +} + +func checkArtifact( + ctx context.Context, + config_obj *config_proto.Config, + artifact string) (*launcher.AnalysisState, error) { + + state := launcher.NewAnalysisState(artifact) + manager, err := services.GetRepositoryManager(config_obj) + if err != nil { + return nil, err + } + + repository, err := manager.GetGlobalRepository(config_obj) + if err != nil { + return nil, err + } + + // Load it into a local repository for checking - this will + // not commit it to the global repository yet + local_repository := manager.NewRepository() + local_repository.SetParent(repository, config_obj) + + artifact_obj, err := local_repository.LoadYaml(artifact, + services.ArtifactOptions{ + ValidateArtifact: true, + }) + + if err != nil { + return &launcher.AnalysisState{ + Errors: []string{err.Error()}, + }, nil + } + + // Verify the artifact + launcher.VerifyArtifact( + ctx, config_obj, repository, artifact_obj, state) + + return state, nil } func getReportArtifacts( + ctx context.Context, config_obj *config_proto.Config, report_type string, number_of_results uint64) ( @@ -158,18 +224,22 @@ func getReportArtifacts( number_of_results = 100 } - manager, err := services.GetRepositoryManager() + manager, err := services.GetRepositoryManager(config_obj) if err != nil { - return nil, err + return nil, Status(config_obj.Verbose, err) } repository, err := manager.GetGlobalRepository(config_obj) if err != nil { - return nil, err + return nil, Status(config_obj.Verbose, err) } result := &artifacts_proto.ArtifactDescriptors{} - for _, name := range repository.List() { - artifact, pres := repository.Get(config_obj, name) + names, err := repository.List(ctx, config_obj) + if err != nil { + return nil, Status(config_obj.Verbose, err) + } + for _, name := range names { + artifact, pres := repository.Get(ctx, config_obj, name) if pres { for _, report := range artifact.Reports { if report.Type == report_type { @@ -187,92 +257,353 @@ func getReportArtifacts( return result, nil } -func searchArtifact( - config_obj *config_proto.Config, - terms []string, - artifact_type string, - number_of_results uint64, fields *api_proto.FieldSelector) ( - *artifacts_proto.ArtifactDescriptors, error) { +type matchPlan struct { + // These must match against the artifact name + name_regex []*regexp.Regexp - if config_obj.GUI == nil { - return nil, errors.New("GUI not configured") + // These must match against the artifact preconditions + precondition_regex []*regexp.Regexp + + tool_regex []*regexp.Regexp + + // Acceptable types + types []string + + // Show hidden artifacts + hidden bool + + // Show empty artifacts (those without sources) + empty_source bool + + builtin *bool + + // Show basic artifacts + basic *bool + + tags []string +} + +func (self *matchPlan) matchTag(artifact *artifacts_proto.Artifact) bool { + if len(self.tags) == 0 { + return true } - name_filter_regexp := config_obj.GUI.ArtifactSearchFilter - if name_filter_regexp == "" { - name_filter_regexp = "." + if artifact.Metadata == nil || len(artifact.Metadata.Tags) == 0 { + return false } - name_filter := regexp.MustCompile(name_filter_regexp) - artifact_type = strings.ToLower(artifact_type) + for _, i := range self.tags { + for _, j := range artifact.Metadata.Tags { + if strings.EqualFold(i, j) { + return true + } + } + } - if number_of_results == 0 { - number_of_results = 1000 + return false +} + +func (self *matchPlan) matchDescOrName(artifact *artifacts_proto.Artifact) bool { + // If no name regexp are specified we do not reject based on name. + if len(self.name_regex) == 0 { + return true } - result := &artifacts_proto.ArtifactDescriptors{} - regexes := []*regexp.Regexp{} - for _, term := range terms { - if len(term) <= 2 { - continue + // All regex must match the same artifact - either in the name or + // description. + matches := 0 + for _, re := range self.name_regex { + if re.MatchString(artifact.Name) { + matches++ + } else if re.MatchString(artifact.Description) { + matches++ } + } + return matches == len(self.name_regex) +} - re, err := regexp.Compile("(?i)" + term) - if err == nil { - regexes = append(regexes, re) +func (self *matchPlan) matchTool(artifact *artifacts_proto.Artifact) bool { + if len(self.tool_regex) == 0 { + return true + } + + if len(artifact.Tools) == 0 { + return false + } + + for _, re := range self.tool_regex { + for _, t := range artifact.Tools { + if re.MatchString(t.Name) { + return true + } } } + return false +} - if len(regexes) == 0 { - return result, nil +// Preconditions can exist at the artifact level or at each source. +func (self *matchPlan) matchPreconditions(artifact *artifacts_proto.Artifact) bool { + if len(self.precondition_regex) == 0 { + return true } - matcher := func(text string, regexes []*regexp.Regexp) bool { - for _, re := range regexes { - if re.FindString(text) == "" { - return false + for _, re := range self.precondition_regex { + if artifact.Precondition != "" && + re.MatchString(artifact.Precondition) { + return true + } + for _, s := range artifact.Sources { + if s.Precondition != "" && + re.MatchString(s.Precondition) { + return true } } + } + return false +} + +func (self *matchPlan) matchBuiltin(artifact *artifacts_proto.Artifact) bool { + if self.builtin == nil { return true } - manager, err := services.GetRepositoryManager() + if *self.builtin { + return artifact.BuiltIn + } + return !artifact.BuiltIn +} + +func (self *matchPlan) matchMetadata(artifact *artifacts_proto.Artifact) bool { + if self.basic == nil { + return true + } + + if *self.basic && artifact.Metadata != nil && + artifact.Metadata.Basic { + return true + } + return false +} + +func (self *matchPlan) matchType(artifact *artifacts_proto.Artifact) bool { + if len(self.types) > 0 { + for _, t := range self.types { + if strings.ToLower(artifact.Type) == t { + return true + } + } + return false + } + return true +} + +func (self *matchPlan) hideEmptySources() bool { + // User wants to show empty sources + if self.empty_source { + return false + } + + // Tag searches should show all artifacts - including ones without + // sources. + if len(self.tags) > 0 { + return false + } + + return true +} + +// All conditions must match +func (self *matchPlan) matchArtifact(artifact *artifacts_proto.Artifact) bool { + if !self.hidden && // Dont show hidden artifacts + + // Artifact is set to hidden + artifact.Metadata != nil && artifact.Metadata.Hidden { + return false + } + + if self.hideEmptySources() && len(artifact.Sources) == 0 { + return false + } + + if !self.matchType(artifact) { + return false + } + + if !self.matchDescOrName(artifact) { + return false + } + + if !self.matchTag(artifact) { + return false + } + + if !self.matchPreconditions(artifact) { + return false + } + + if !self.matchBuiltin(artifact) { + return false + } + + if !self.matchMetadata(artifact) { + return false + } + + if !self.matchTool(artifact) { + return false + } + + return true +} + +func prepareMatchPlan(search string) *matchPlan { + result := &matchPlan{} + // Tokenise the search expression into search terms: + for _, token := range strings.Split(search, " ") { + if token == "" { + continue + } + + parts := strings.SplitN(token, ":", 2) + if len(parts) == 2 { + verb := parts[0] + term := parts[1] + switch verb { + case "empty": + if term == "true" { + result.empty_source = true + } + continue + + case "hidden": + if term == "true" { + result.hidden = true + } + continue + + case "type": + result.types = append(result.types, + strings.ToLower(term)) + continue + + case "precondition": + re, err := regexp.Compile("(?i)" + term) + if err == nil { + result.precondition_regex = append( + result.precondition_regex, re) + } + continue + + case "tool": + re, err := regexp.Compile("(?i)" + term) + if err == nil { + result.tool_regex = append( + result.tool_regex, re) + } + continue + + case "builtin": + value := false + if term == "yes" { + value = true + } + result.builtin = &value + continue + + case "metadata": + if term == "basic" { + value := true + result.basic = &value + } + continue + + case "tag": + result.tags = append(result.tags, strings.ToLower(term)) + continue + } + } + re, err := regexp.Compile("(?i)" + token) + if err == nil { + result.name_regex = append( + result.name_regex, re) + } + } + return result +} + +func searchArtifact( + ctx context.Context, + config_obj *config_proto.Config, + search_term string, + artifact_type string, + number_of_results uint64, fields *api_proto.FieldSelector) ( + *artifacts_proto.ArtifactDescriptors, error) { + + if config_obj.GUI == nil { + return nil, InvalidStatus("GUI not configured") + } + + matcher := prepareMatchPlan(search_term) + if artifact_type != "" { + matcher.types = append(matcher.types, strings.ToLower(artifact_type)) + } + + if number_of_results == 0 { + number_of_results = 1000 + } + + result := &artifacts_proto.ArtifactDescriptors{} + manager, err := services.GetRepositoryManager(config_obj) if err != nil { - return nil, err + return nil, Status(config_obj.Verbose, err) } repository, err := manager.GetGlobalRepository(config_obj) if err != nil { - return nil, err + return nil, Status(config_obj.Verbose, err) } - for _, name := range repository.List() { - if name_filter.FindString(name) == "" { + names, err := repository.List(ctx, config_obj) + if err != nil { + return nil, Status(config_obj.Verbose, err) + } + + for _, name := range names { + artifact, pres := repository.Get(ctx, config_obj, name) + if !pres { continue } - artifact, pres := repository.Get(config_obj, name) - if pres { - // Skip non matching types - if artifact_type != "" && - artifact.Type != artifact_type { - continue - } + if matcher.matchArtifact(artifact) { + if fields == nil { + result.Items = append(result.Items, artifact) + } else { + // Send back minimal information about the + // artifacts + new_item := &artifacts_proto.Artifact{} + if fields.Name { + new_item.Name = artifact.Name + new_item.BuiltIn = artifact.BuiltIn + new_item.IsInherited = artifact.IsInherited + } - if matcher(artifact.Description, regexes) || - matcher(artifact.Name, regexes) { - if fields == nil { - result.Items = append(result.Items, artifact) - } else { - // Send back minimal information about the - // artifacts - new_item := &artifacts_proto.Artifact{} - if fields.Name { - new_item.Name = artifact.Name - new_item.BuiltIn = artifact.BuiltIn - } + if fields.Description { + new_item.Description = artifact.Description + } + if fields.Type { + new_item.Type = artifact.Type + } - result.Items = append(result.Items, new_item) + if fields.Sources { + for _, s := range artifact.Sources { + new_item.Sources = append(new_item.Sources, + &artifacts_proto.ArtifactSource{ + Name: s.Name, + Description: s.Description, + }) + } } + + result.Items = append(result.Items, new_item) } } @@ -281,71 +612,130 @@ func searchArtifact( } } + if fields != nil && fields.Tags { + result.Tags, err = repository.Tags(ctx, config_obj) + if err != nil { + return nil, Status(config_obj.Verbose, err) + } + } + return result, nil } func (self *ApiServer) LoadArtifactPack( ctx context.Context, - in *api_proto.VFSFileBuffer) ( - *api_proto.LoadArtifactPackResponse, error) { + in *api_proto.LoadArtifactPackRequest) ( + res *api_proto.LoadArtifactPackResponse, err error) { + + defer Instrument("LoadArtifactPack")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + users_manager := services.GetUserManager() + user_record, org_config_obj, err := users_manager.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } + principal := user_record.Name permissions := acls.SERVER_ARTIFACT_WRITER - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to upload artifact packs.") } - prefix := constants.ARTIFACT_PACK_NAME_PREFIX + prefix := in.Prefix + var filter_re *regexp.Regexp + if in.Filter != "" { + filter_re, err = regexp.Compile("(?i)" + in.Filter) + if err != nil { + return nil, Status(self.verbose, err) + } + } - result := &api_proto.LoadArtifactPackResponse{} - buffer := bytes.NewReader(in.Data) - zip_reader, err := zip.NewReader(buffer, int64(len(in.Data))) + zip_reader, closer, err := getZipReader(ctx, org_config_obj, in) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } + defer func() { + err1 := closer() + if err != nil { + err = err1 + } + }() + result := &api_proto.LoadArtifactPackResponse{ + VfsPath: in.VfsPath, + } for _, file := range zip_reader.File { - if strings.HasSuffix(file.Name, ".yaml") { + if strings.HasSuffix(file.Name, ".yaml") || + strings.HasSuffix(file.Name, ".yml") { fd, err := file.Open() if err != nil { continue } - data, err := ioutil.ReadAll(fd) + data, err := utils.ReadAllWithLimit(fd, constants.MAX_MEMORY) fd.Close() if err != nil { continue } - // Make sure the artifact is written into the - // Packs part to prevent clashes with built in - // names. + // Update the definition to include the prefix on the + // artifact name. artifact_definition := ensureArtifactPrefix( - string(data), prefix) + string(data), in.Prefix) request := &api_proto.SetArtifactRequest{ - Op: api_proto.SetArtifactRequest_SET, + Op: api_proto.SetArtifactRequest_CHECK, Artifact: artifact_definition, + Tags: in.Tags, + } + + definition, err := setArtifactFile(ctx, + org_config_obj, principal, request, prefix) + if err != nil { + if len(result.Errors) < 10 { + result.Errors = append(result.Errors, &api_proto.LoadArtifactError{ + Filename: file.Name, + Error: err.Error(), + }) + + } else if len(result.Errors) == 10 { + result.Errors = append(result.Errors, &api_proto.LoadArtifactError{ + Filename: file.Name, + Error: "Too many errors - suppressing", + }) + } + continue + } + + if filter_re != nil && !filter_re.MatchString(definition.Name) { + continue + } + + if !in.ReallyDoIt { + result.SuccessfulArtifacts = append(result.SuccessfulArtifacts, + definition.Name) + continue } - definition, err := setArtifactFile( - self.config, user_name, request, prefix) + request.Op = api_proto.SetArtifactRequest_SET + + // Set the artifact for real. + definition, err = setArtifactFile(ctx, + org_config_obj, principal, request, prefix) if err == nil { - logging.GetLogger(self.config, &logging.Audit). - WithFields(logrus.Fields{ - "user": user_name, - "artifact": definition.Name, - "details": fmt.Sprintf( - "%v", request.Artifact), - }).Info("LoadArtifactPack") + err := services.LogAudit(ctx, + org_config_obj, principal, "LoadArtifactPack", + ordereddict.NewDict(). + Set("artifact", definition.Name). + Set("details", request.Artifact)) + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.FrontendComponent) + logger.Error("LoadArtifactPack %v %v", + principal, definition.Name) + } result.SuccessfulArtifacts = append(result.SuccessfulArtifacts, definition.Name) @@ -361,6 +751,71 @@ func (self *ApiServer) LoadArtifactPack( return result, nil } +func getZipReader( + ctx context.Context, + config_obj *config_proto.Config, + in *api_proto.LoadArtifactPackRequest) (*zip.Reader, func() error, error) { + + // Create a temp file and store the data in it. + if len(in.Data) > 0 { + // Check the file is a valid zip file first, before we cache + // it locally. + buffer := bytes.NewReader(in.Data) + zipfd, err := zip.NewReader(buffer, int64(len(in.Data))) + if err != nil { + return nil, nil, err + } + + path_manager := paths.NewTempPathManager("") + file_store_factory := file_store.GetFileStore(config_obj) + fd, err := file_store_factory.WriteFile(path_manager.Path()) + if err != nil { + return nil, nil, err + } + defer fd.Close() + + _, err = utils.Copy(ctx, fd, bytes.NewReader(in.Data)) + if err != nil { + return nil, nil, err + } + in.VfsPath = path_manager.Path().Components() + return zipfd, func() error { return nil }, nil + } + + // Otherwise open the filestore path + if len(in.VfsPath) < 2 { + return nil, nil, errors.New("vfs_path should be specified") + } + + if in.VfsPath[0] != paths.TEMP_ROOT.Components()[0] && + in.VfsPath[0] != paths.PUBLIC_ROOT.Components()[0] { + return nil, nil, errors.New("vfs_path should be a temp path") + } + + pathspec := path_specs.NewUnsafeFilestorePath(in.VfsPath...). + SetType(api.PATH_TYPE_FILESTORE_ANY) + + err := file_store_accessor.IsFileAccessible(pathspec) + if err != nil { + return nil, nil, err + } + + file_store_factory := file_store.GetFileStore(config_obj) + fd, err := file_store_factory.ReadFile(pathspec) + if err != nil { + return nil, nil, err + } + + stat, err := fd.Stat() + if err != nil { + return nil, nil, err + } + + zip_reader, err := zip.NewReader( + utils.MakeReaderAtter(fd), stat.Size()) + return zip_reader, fd.Close, err +} + // MakeCollectorRequest is a convenience function for creating // flows_proto.ArtifactCollectorArgs protobufs. func MakeCollectorRequest( diff --git a/api/assets.go b/api/assets.go index eac927f9b..9f8084203 100644 --- a/api/assets.go +++ b/api/assets.go @@ -1,6 +1,6 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -20,26 +20,43 @@ package api import ( + "bytes" + "context" + "fmt" "html/template" "net/http" + "strings" "time" + "github.com/andybalholm/brotli" + errors "github.com/go-errors/errors" "github.com/gorilla/csrf" + "github.com/lpar/gzipped" "www.velocidex.com/golang/velociraptor/api/proto" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" + utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/gui/velociraptor" gui_assets "www.velocidex.com/golang/velociraptor/gui/velociraptor" - users "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services" + vutils "www.velocidex.com/golang/velociraptor/utils" ) -func install_static_assets(config_obj *config_proto.Config, mux *http.ServeMux) { - base := "" - if config_obj.GUI != nil { - base = config_obj.GUI.BasePath - } - dir := base + "/app/" - mux.Handle(dir, http.StripPrefix(dir, http.FileServer(gui_assets.HTTP))) +var ( + UnauthenticatedAccessError = errors.New("Unauthenticated access") +) + +func install_static_assets( + ctx context.Context, + config_obj *config_proto.Config, mux *api_utils.ServeMux) { + base := utils.GetBasePath(config_obj) + dir := utils.Join(base, "/app/") + mux.Handle(dir, ipFilter(config_obj, api_utils.StripPrefix( + dir, fixCSSURLs(config_obj, + gzipped.FileServer(NewCachedFilesystem(ctx, gui_assets.NewHTTPFS())))))) + mux.Handle("/favicon.png", - http.RedirectHandler(base+"/static/images/favicon.ico", + http.RedirectHandler(utils.Join(base, "/favicon.ico"), http.StatusMovedPermanently)) } @@ -51,7 +68,7 @@ func GetTemplateHandler( // app. This is not a fatal error but it is not very useful :-). data = []byte( ` -

This binary was not build with GUI support!

+

This binary was not built with GUI support!

Search for building instructions on https://docs.velociraptor.app/ `) @@ -62,33 +79,111 @@ func GetTemplateHandler( return nil, err } - base := config_obj.GUI.BasePath + return api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + userinfo := GetUserInfo(r.Context(), config_obj) - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userinfo := GetUserInfo(r.Context(), config_obj) + // This should never happen! + if userinfo.Name == "" { + returnError(config_obj, w, 401, UnauthenticatedAccessError) + return + } - // This should never happen! - if userinfo.Name == "" { - returnError(w, 401, "Unauthenticated access.") - return - } + users := services.GetUserManager() + user_options, err := users.GetUserOptions(r.Context(), userinfo.Name) + if err != nil { + // Options may not exist yet + user_options = &proto.SetGUIOptionsRequest{} + } - user_options, err := users.GetUserOptions(config_obj, userinfo.Name) - if err != nil { - // Options may not exist yet - user_options = &proto.SetGUIOptionsRequest{} - } + args := velociraptor.HTMLtemplateArgs{ + Timestamp: time.Now().UTC().UnixNano() / 1000, + CsrfToken: csrf.Token(r), + BasePath: utils.GetBasePath(config_obj), + Heading: "Heading", + UserTheme: user_options.Theme, + OrgId: user_options.Org, + } + err = tmpl.Execute(w, args) + if err != nil { + w.WriteHeader(500) + } + }), nil +} - args := _templateArgs{ - Timestamp: time.Now().UTC().UnixNano() / 1000, - CsrfToken: csrf.Token(r), - BasePath: base, - Heading: "Heading", - UserTheme: user_options.Theme, - } - err = tmpl.Execute(w, args) - if err != nil { - w.WriteHeader(500) +// Vite hard compiles the css urls into the bundle so we can not move +// the base_path. This handler fixes this. +func fixCSSURLs(config_obj *config_proto.Config, + parent http.Handler) http.Handler { + + if config_obj.GUI == nil || config_obj.GUI.BasePath == "" { + return api_utils.HandlerFunc(parent, parent.ServeHTTP). + AddChild("NewInterceptingResponseWriter") + } + + return api_utils.HandlerFunc(parent, + func(w http.ResponseWriter, r *http.Request) { + if !strings.HasSuffix(r.URL.Path, ".css") { + parent.ServeHTTP(w, r) + } else { + parent.ServeHTTP( + NewInterceptingResponseWriter(config_obj, w, r), r) + } + }).AddChild("NewInterceptingResponseWriter") +} + +type interceptingResponseWriter struct { + http.ResponseWriter + + from, to string + + br_writer *brotli.Writer +} + +// Replace base path in the CSS url properties. +func (self *interceptingResponseWriter) Write(buf []byte) (int, error) { + new_buf := bytes.ReplaceAll(buf, []byte(self.from), []byte(self.to)) + // No compression + if self.br_writer == nil { + _, err := self.ResponseWriter.Write(new_buf) + return len(buf), err + } + + // Implement brotli compression + _, err := self.br_writer.Write(new_buf) + if err != nil { + return 0, err + } + err = self.br_writer.Flush() + return len(buf), err +} + +func NewInterceptingResponseWriter( + config_obj *config_proto.Config, + w http.ResponseWriter, r *http.Request) http.ResponseWriter { + + // Try to do brotli compression if it is available. + accept_encoding, pres := r.Header["Accept-Encoding"] + if pres && len(accept_encoding) > 0 { + parts := strings.Split(accept_encoding[0], ", ") + if vutils.InString(parts, "br") { + w.Header()["Content-Encoding"] = []string{"br"} + + return &interceptingResponseWriter{ + ResponseWriter: w, + from: "url(/app/assets/", + to: fmt.Sprintf("url(%v/app/assets/", + utils.GetBasePath(config_obj)), + br_writer: brotli.NewWriter(w), + } } - }), nil + } + + // Otherwise just pass through + return &interceptingResponseWriter{ + ResponseWriter: w, + from: "url(/app/assets/", + to: fmt.Sprintf("url(%v/app/assets/", + utils.GetBasePath(config_obj)), + } } diff --git a/api/auth.go b/api/auth.go index cdb03f3a6..93cf49b03 100644 --- a/api/auth.go +++ b/api/auth.go @@ -1,105 +1,36 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package api import ( - "context" - "crypto/x509" - - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/peer" api_proto "www.velocidex.com/golang/velociraptor/api/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" - crypto_utils "www.velocidex.com/golang/velociraptor/crypto/utils" - "www.velocidex.com/golang/velociraptor/json" - "www.velocidex.com/golang/velociraptor/logging" ) -// GetGRPCUserInfo: Extracts user information from GRPC context. -func GetGRPCUserInfo( - config_obj *config_proto.Config, - ctx context.Context, - ca_pool *x509.CertPool) *api_proto.VelociraptorUser { - result := &api_proto.VelociraptorUser{} - - // Check for remote TLS client certs. - peer, ok := peer.FromContext(ctx) - if ok { - tlsInfo, ok := peer.AuthInfo.(credentials.TLSInfo) - if ok && config_obj.API != nil { - // Extract the name from each incoming certificate - for _, peer_cert := range tlsInfo.State.PeerCertificates { - - // This certificate is not valid, skip it. - chains, err := peer_cert.Verify( - x509.VerifyOptions{Roots: ca_pool}) - if err != nil || len(chains) == 0 { - continue - } - - result.Name = crypto_utils.GetSubjectName( - tlsInfo.State.PeerCertificates[0]) - - // Calls from the gRPC gateway are allowed to - // embed the authenticated web user in the - // metadata. This allows the API gateway to - // impersonate anyone - it must be trusted to - // convert web side authentication to a valid - // user name which it may pass in the call - // context. - if result.Name == config_obj.API.PinnedGwName { - md, ok := metadata.FromIncomingContext(ctx) - if ok { - userinfo := md.Get("USER") - if len(userinfo) > 0 { - data := []byte(userinfo[0]) - err := json.Unmarshal(data, result) - if err != nil { - logger := logging.GetLogger(config_obj, - &logging.FrontendComponent) - logger.Error("GetGRPCUserInfo: %v", err) - result.Name = "" - } - } - } - } - } - } - } - - return result -} - -func NewDefaultUserObject(config_obj *config_proto.Config) *api_proto.ApiGrrUser { - result := &api_proto.ApiGrrUser{ - UserType: api_proto.ApiGrrUser_USER_TYPE_ADMIN, +func NewDefaultUserObject(config_obj *config_proto.Config) *api_proto.ApiUser { + result := &api_proto.ApiUser{ + UserType: api_proto.ApiUser_USER_TYPE_ADMIN, + InterfaceTraits: &api_proto.ApiUserInterfaceTraits{}, } if config_obj.GUI != nil { - result.InterfaceTraits = &api_proto.ApiGrrUserInterfaceTraits{ - AuthUsingGoogle: config_obj.GUI.GoogleOauthClientId != "", - Links: []*api_proto.UILink{}, - } - - for _, link := range config_obj.GUI.Links { - result.InterfaceTraits.Links = append(result.InterfaceTraits.Links, - &api_proto.UILink{Text: link.Text, Url: link.Url}) + result.InterfaceTraits = &api_proto.ApiUserInterfaceTraits{ + Links: config_obj.GUI.Links, } } diff --git a/api/authenticators/auth.go b/api/authenticators/auth.go index 2953efef5..23d7b3198 100644 --- a/api/authenticators/auth.go +++ b/api/authenticators/auth.go @@ -1,21 +1,51 @@ package authenticators import ( + "context" + "crypto/x509" "errors" + "fmt" "net/http" "strings" + "sync" + "www.velocidex.com/golang/velociraptor/acls" + utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" ) +var ( + mu sync.Mutex + + // Factory dispatcher + auth_dispatcher = make(map[string]func( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error)) + + auth_cache Authenticator +) + +func ResetAuthCache() { + mu.Lock() + defer mu.Unlock() + auth_cache = nil +} + // All SSO Authenticators implement this interface. type Authenticator interface { - AddHandlers(config_obj *config_proto.Config, mux *http.ServeMux) error - AuthenticateUserHandler( - config_obj *config_proto.Config, - parent http.Handler) http.Handler + AddHandlers(mux *utils.ServeMux) error + AddLogoff(mux *utils.ServeMux) error + + // Make sure the user is authenticated and has the required + // permission access to the requested org. (usually this is + // acls.READ_RESULTS) + AuthenticateUserHandler(parent http.Handler, + permission acls.ACL_PERMISSION) http.Handler IsPasswordLess() bool + RequireClientCerts() bool + AuthRedirectTemplate() string } func NewAuthenticator(config_obj *config_proto.Config) (Authenticator, error) { @@ -25,19 +55,199 @@ func NewAuthenticator(config_obj *config_proto.Config) (Authenticator, error) { return nil, errors.New("GUI not configured") } - switch strings.ToLower(config_obj.GUI.Authenticator.Type) { - case "azure": - return &AzureAuthenticator{}, nil - case "github": - return &GitHubAuthenticator{}, nil - case "google": - return &GoogleAuthenticator{}, nil - case "saml": - return &SamlAuthenticator{}, nil - case "basic": - return &BasicAuthenticator{}, nil - case "oidc": - return &OidcAuthenticator{}, nil + mu.Lock() + cached := auth_cache + mu.Unlock() + + if cached != nil { + return cached, nil + } + + ctx, err := ClientContext(context.Background(), config_obj, + DefaultTransforms(config_obj, config_obj.GUI.Authenticator)) + if err != nil { + return nil, err + } + + new_auth, err := getAuthenticatorByType( + ctx, config_obj, config_obj.GUI.Authenticator) + if err == nil { + mu.Lock() + auth_cache = new_auth + mu.Unlock() + } + + return new_auth, err +} + +func RegisterAuthenticator(name string, + handler func( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error)) { + mu.Lock() + defer mu.Unlock() + + auth_dispatcher[strings.ToLower(name)] = handler +} + +func getAuthenticatorByType( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error) { + + mu.Lock() + key := strings.ToLower(auth_config.Type) + handler, pres := auth_dispatcher[key] + mu.Unlock() + if pres { + // Make sure the dispatcher lock is unlocked during call to + // handler - the multi authenticator needs to access the + // other types. + return handler(ctx, config_obj, auth_config) } return nil, errors.New("No valid authenticator found") } + +func configRequirePublicUrl(config_obj *config_proto.Config) error { + if config_obj.GUI.PublicUrl == "" { + return fmt.Errorf("Authentication type `%s' requires valid public_url parameter", + config_obj.GUI.Authenticator.Type) + } + return nil + +} + +func init() { + RegisterAuthenticator("azure", func( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error) { + err := configRequirePublicUrl(config_obj) + if err != nil { + return nil, err + } + router := &AzureOidcRouter{ + config_obj: config_obj, + authenticator: auth_config, + } + claims_getter := &AzureClaimsGetter{ + config_obj: config_obj, + router: router, + } + return NewOidcAuthenticator( + config_obj, auth_config, router, claims_getter), nil + }) + + RegisterAuthenticator("github", func( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error) { + err := configRequirePublicUrl(config_obj) + if err != nil { + return nil, err + } + + router := &GithubOidcRouter{ + config_obj: config_obj, + } + claims_getter := &GithubClaimsGetter{ + config_obj: config_obj, + } + return NewOidcAuthenticator( + config_obj, auth_config, router, claims_getter), nil + }) + + // This is now basically an alias for a generic OIDC connector + // since Google is pretty good about following the standards. + RegisterAuthenticator("google", func( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error) { + err := configRequirePublicUrl(config_obj) + if err != nil { + return nil, err + } + + router := &GoogleOidcRouter{ + config_obj: config_obj, + } + + claims_getter, err := NewOidcClaimsGetter( + ctx, config_obj, auth_config, router) + if err != nil { + return nil, err + } + + return NewOidcAuthenticator( + config_obj, auth_config, router, claims_getter), nil + }) + + RegisterAuthenticator("saml", func( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error) { + return NewSamlAuthenticator(config_obj, auth_config) + }) + + RegisterAuthenticator("basic", func( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error) { + return &BasicAuthenticator{ + config_obj: config_obj, + }, nil + }) + + RegisterAuthenticator("certs", func( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error) { + if config_obj.GUI == nil || config_obj.GUI.UsePlainHttp { + return nil, errors.New("'Certs' authenticator must use TLS!") + } + + result := &CertAuthenticator{ + config_obj: config_obj, + x509_roots: x509.NewCertPool(), + default_roles: auth_config.DefaultRolesForUnknownUser, + } + if config_obj.Client != nil { + result.x509_roots.AppendCertsFromPEM([]byte( + config_obj.Client.CaCertificate)) + } + + return result, nil + }) + + RegisterAuthenticator("oidc", func( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error) { + err := configRequirePublicUrl(config_obj) + if err != nil { + return nil, err + } + + router := &DefaultOidcRouter{ + authenticator: auth_config, + config_obj: config_obj, + } + + claims_getter, err := NewOidcClaimsGetter( + ctx, config_obj, auth_config, router) + if err != nil { + return nil, err + } + + return NewOidcAuthenticator( + config_obj, auth_config, router, claims_getter), nil + }) + + RegisterAuthenticator("multi", func( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error) { + return NewMultiAuthenticator(ctx, config_obj, auth_config) + }) +} diff --git a/api/authenticators/azure.go b/api/authenticators/azure.go index 39226773c..4f9cfffa0 100644 --- a/api/authenticators/azure.go +++ b/api/authenticators/azure.go @@ -1,192 +1,107 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package authenticators import ( - "errors" + "encoding/base64" "fmt" - "io" - "io/ioutil" "net/http" - "time" - jwt "github.com/golang-jwt/jwt" - "github.com/sirupsen/logrus" - context "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/microsoft" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/json" - "www.velocidex.com/golang/velociraptor/logging" + utils "www.velocidex.com/golang/velociraptor/utils" +) + +const ( + AzureIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpUWqDnYQcchQxcEuKiJOtQpFqBBqhVYdTC79giYNSYuLo+BacPBjserg4qyrg6sgCH6AuAtOii5S4v+SQosYD4778e7e4+4dIDTKTLO6YoCmV81UIi5msqti4BUhBNGHWYzJzDLmJCkJz/F1Dx9f76I8y/vcn6NXzVkM8InEMWaYVeIN4unNqsF5nzjMirJKfE48btIFiR+5rrj8xrngsMAzw2Y6NU8cJhYLHax0MCuaGvEUcUTVdMoXMi6rnLc4a+Uaa92TvzCU01eWuU5zGAksYgkSRCiooYQyqojSqpNiIUX7cQ//kOOXyKWQqwRGjgVUoEF2/OB/8LtbKz854SaF4kD3i21/jACBXaBZt+3vY9tungD+Z+BKb/srDWDmk/R6W4scAf3bwMV1W1P2gMsdYPDJkE3Zkfw0hXweeD+jb8oCA7dAz5rbW2sfpw9AmrpK3gAHh8BogbLXPd4d7Ozt3zOt/n4A6eJy1kar81QAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfpDAgOOyNpkJZEAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAACbxJREFUaN7tWltsFNcZ/s45s7u2WWwcg20uAXNJSxMCbRNIQhI1LSlV2rSqWrUF5SUvfcpLlYe+t5GivrbqS9WLGiWRmiqqIiQIEJMACaGAsU1siPEVY6/Xu971eu+XmTmnD7uzczuzuybYCJXRvOyxd/b/zv/93385Q4QQuJ8vivv8egDgXl/KV/y+zkUmX9QFhN/X+LcIIYQSAjQR0kTvHYBUtvDPY//95MpYWtf0vbux73GwBswRUBTmY5QA2/301xsD+4LKvQHw71NXX3/7oq6VoKsYvoXWduzaUdt0ABACKqByCCDPRwr8vd3BDh9Z7RjI5EtvnxzSQUAYCIMODF6DVJRF9RaVfzBXcK6gDyXVexDEw6OzfZEMCAUt3wr6vkQu52m3EJWP3LaoCXwYK+lidQEI4Mzl0RwHCAGhIBSUoaBjYtI0HXa7q2DgXPxPRo8U+aoCiC1lP+yfLQsKKAVloBRMQf8wdC6zG5JFVBbHNX51SV1VANcnwxfmMyDE9ABhoAomQ0gkpFSRgOEmqU4l1BJfLQCazs8P3ILOAQIQIwwYKEO2hKkZL6rI/SAAIU6ktXBRXyUAiXT++NXbxvYTACCs4gTCcHMcmuqiSi0KAZgq8YE70qI7AfDlrejlcBqEGB4goNVI8GHsNlIZF1VkFDJ1SUCI3lhJFSsPQAh81j8FgYrpFSdQUFbhUrqA2ZDNPnjYbXfL+aR2B1q0bADJbOH4pSnD7nL6NEK57ATmx/URM0Dl1BcuhGJY48OLpRUHMDwauhLLm+RxOKF835hCOiNRGy8PcAEhhBCno4Xlsmh5ALgQH12eUAUMu1FBAgJqyWiqwMSEY4O9PVDWK0Dg3aQ2n9NWEEA0kT02OOcij4GEGE5gCoZGoHEpVSxInMEd1cWlxZJYOQBD4/OD8byLPMQMgwoGBbNRJBalVKmd2o5HiyoXKwJA0/mZ/mlU+VMlD0HlIyEmgIKO6RkvqtSI79Mp7XZOXxEAC8nciWtzht2QeYCAWpwwNglVq5F9ZalNzKm8L15cEQDDE9HheM5ptwNJtbpmCqbnkUrZTOQ1SyMhyuVTb7RYbJhFtHH9Odt/S8CoHUA8kNBKWUEZChpCIXsr45XabEXHhaQ21zCLGgUQT+Z7B0K2LTca9IoKmVwqV6YMig8jY+C8Uf4Y6yMqvxYr3mUAQ2PhK0slJ1ukYUCIUVYoZCrcms16UcUFxsTTGy40yKKGAOhc9F6ZEsKSthxZzFaZVuOY7e9s/ePX2r2oUkOX/rKkhjPaXQMwH8+8fy1qp4ori1nZRSstzpFDe5/ZFKTE0SJb8MhdAU0Xn0eL4m4BGByPjGWKFvIQT/KYLQ6Dohz89iOb1yhHg0r97syxyMUH4UKxgVa/PgBV56f7ZyCM7acOW60xbcVGD/V07Hp4fYtCvrfe79mdwSO+gbMpdSqt3QUA4cXsyRtRG3lMJHA6wWQXO/zEtnXBJgo82eHfTEm9xtLph5gqLkULdwHAF+OR8WTRXvDYuWT1ieGBJr/y3N6tjFIAO9b6DrYweRXEPfVUCNEbLuTqsagOAI2LM/0z3JZu4SyBQNw+eW7j2q/3dJYfElTI9zcEiAdVaqS2i0l1ph6L6gBYWMqdu7HgWTg4s1hVhcjhb27qWNtUfc7zXU3riMVE7pjSyUN5UuWDkfxXAnBtdH4grTamPAa7QNZQeujJ7dbn7GxVXgwqponu6Z2HnvaGCoWaLKK19af3yrSldpBmMRe7CNnfHdyzs9P6KB8lr2xpriWdHqntb4ul2ZR6hwDCscw7N+IWQ6VZzMUu4Ojz232MOZ62r7OplcLUU6/pL3e2OJ/N5cUdABBA32gkktMt88PGspjCDjy2hbjG/Z0t7FfrfKZ93Mtup8i+P5svaGLZBxyqxk/0z1WoUiZrWW0gIAgAUAp/s1mWGj+xp70pnGf5iZS5FUJAcMH11lIBghqLcHzRPFuw/+nzpDq2VNq7PrA8ADOxTO/YosENYcAwVBRAWxeCD0n6HuDlf82aDhAcugqtgFKeB3TsfxRMkdjtjWdJw4W5/OPrA2RZFBoYi85kVXv9bC0cCITnFI0DevUmRAfVCdMpE5kSMjnv2Zb8VEEIfBzKZ1S+jBgo6fzM4ByHK2StjXw6hswi6h/0GyMjqoAwLCSkB021Sm4hLi6p0x6jXzmAyGLu07GEc8utikkIBJBaQCZeH0OlQ6BQ/IgkoHPJ6QF3HUBZRsIhTQzM5ZYBYGA0cj2rVQaGUqmpNsfpOFILdTBUe2WqIKcik63X4kh09tTtXE7jDQEoavyjvpC8WnbXDgCyCSQj4Lwmi4xOn/oQicmqIO/RCwQE3o2r04lSQwDmYpm/3lySZV/vlVwKyQjAazmhPG5RfFhYgqpJqWLLD6566fztnKgLQACXRqJFlRsNrnvoIPdJi55763DHgVbFm0WswqIiRzrjueWeoxfx3nQu69IiJ4CSqh/rn7eXBg008iA/3tH20wMb//FKzwsdfjmLqs0+URBLSA6aPClU+dif0kZc4xYngKlo5tx0ymPLCUC9Sonv7ukONimPblnz5yPbfrIx4MEiY16USEPVXKcH9hbHpUspnX8642SRE0Df6MJ8Qa+IT+3S37LSHVCe+kY3JYQAj21q+dPRnle3NsvFlFAwBQUNtnlRzRbH0CXBcXYmn7QfQ9kAFFT+8RdR7lV4Onoxy8rTm9fu2tRWfc62jqY/HOl5decaZ0lnPVGOJ9xnlXWmFcDFpdLUYtETwFw8e/5WCoTKTpAgI0+FUT/8VnewyRa+XW3+N3++9bXda+UsYj7Ek9B1eQpztDgWXVrQRN9M1hNA/83oRF63Tz+9hg5mTLcQ8p19m9yc39jm//3Ptv7uiXWujEZBFZQ4Mpm6g153i3NiKpe1HOpTC3/0kwMR2+BW0shLkLy8a9327lapeLa3KK//aMsbT7UHqEuLmA8LcduWo6GD8Q/ipQmLFpkA0tnS3yfTMtNRs4nBL57e4vN+USsYYL/5weY3n+2o/Iv1HC2dA+ceKcw7tQE3LZ2++cMKozubFVBjziNF4mJXV7Nv364NtWu5YIC99uLmtw53GXMXQ4v8zZU2o4FSAhb5bPZTCYC2YOCNl7Z3+hktv5FXvSsdJTEu4yMl7T762xe2busM1p2OBRTyy4Nd77zUtS3AKKWEMvj82NRlM13WEpDybcSin+BoV+DZHaY2EOubu6rOr04ujoXSOhe27khI3n0jhPR0BQ88sr7Zzxo9JuRiaDZ3PZQtlFRdYcLHTNPhaizLG8yo32fq20Mt7JmH12xoYXIA9+P14M3dBwD+3wH8D1fWkeojxAXbAAAAAElFTkSuQmCC" ) type AzureUser struct { - Mail string `json:"userPrincipalName"` - Name string `json:"displayName"` - Token string `json:"token"` + Mail string `json:"userPrincipalName"` + Name string `json:"displayName"` + Picture string `json:"picture"` } -type AzureAuthenticator struct{} - -func (self *AzureAuthenticator) IsPasswordLess() bool { - return true +type AzureOidcRouter struct { + config_obj *config_proto.Config + authenticator *config_proto.Authenticator } -func (self *AzureAuthenticator) AddHandlers(config_obj *config_proto.Config, mux *http.ServeMux) error { - mux.Handle("/auth/azure/login", oauthAzureLogin(config_obj)) - mux.Handle("/auth/azure/callback", oauthAzureCallback(config_obj)) - mux.Handle("/auth/azure/picture", oauthAzurePicture(config_obj)) - - installLogoff(config_obj, mux) - return nil +func (self *AzureOidcRouter) Name() string { + return "Azure" } -// Check that the user is proerly authenticated. -func (self *AzureAuthenticator) AuthenticateUserHandler( - config_obj *config_proto.Config, - parent http.Handler) http.Handler { - - return authenticateUserHandle( - config_obj, parent, "/auth/azure/login", "Microsoft O365/Azure AD") +func (self *AzureOidcRouter) LoginHandler() string { + return "/auth/azure/login" } -func oauthAzureLogin(config_obj *config_proto.Config) http.Handler { - authenticator := config_obj.GUI.Authenticator - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var azureOauthConfig = &oauth2.Config{ - RedirectURL: config_obj.GUI.PublicUrl + "auth/azure/callback", - ClientID: authenticator.OauthClientId, - ClientSecret: authenticator.OauthClientSecret, - Scopes: []string{"User.Read"}, - Endpoint: microsoft.AzureADEndpoint(authenticator.Tenant), - } - - // Create oauthState cookie - oauthState, err := r.Cookie("oauthstate") - if err != nil { - oauthState = generateStateOauthCookie(w) - } - - u := azureOauthConfig.AuthCodeURL(oauthState.Value) - http.Redirect(w, r, u, http.StatusTemporaryRedirect) - }) +func (self *AzureOidcRouter) CallbackHandler() string { + return "/auth/azure/callback" } -func oauthAzureCallback(config_obj *config_proto.Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Read oauthState from Cookie - oauthState, _ := r.Cookie("oauthstate") - - if r.FormValue("state") != oauthState.Value { - logging.GetLogger(config_obj, &logging.GUIComponent). - Error("invalid oauth azure state") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - user_info, err := getUserDataFromAzure( - r.Context(), config_obj, r.FormValue("code")) - if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - WithFields(logrus.Fields{ - "err": err, - }).Error("getUserDataFromAzure") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } +func (self *AzureOidcRouter) Scopes() []string { + return []string{"User.Read"} +} - // Create a new token object, specifying signing method and the claims - // you would like it to contain. - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "user": user_info.Mail, +func (self *AzureOidcRouter) Issuer() string { + return "" +} - // Require re-auth after one day. - "expires": float64(time.Now().AddDate(0, 0, 1).Unix()), - "picture": "/auth/azure/picture", - "token": user_info.Token, - }) +func (self *AzureOidcRouter) Endpoint() oauth2.Endpoint { + return microsoft.AzureADEndpoint(self.authenticator.Tenant) +} - // Sign and get the complete encoded token as a string using the secret - tokenString, err := token.SignedString( - []byte(config_obj.Frontend.PrivateKey)) - if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - WithFields(logrus.Fields{ - "err": err, - }).Error("getUserDataFromAzure") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } +func (self *AzureOidcRouter) SetEndpoint(oauth2.Endpoint) {} - // Set the cookie and redirect. - cookie := &http.Cookie{ - Name: "VelociraptorAuth", - Value: tokenString, - Path: "/", - Secure: true, - HttpOnly: true, - Expires: time.Now().AddDate(0, 0, 1), - } - http.SetCookie(w, cookie) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - }) +func (self *AzureOidcRouter) Avatar() string { + return AzureIcon } -func getAzureOauthConfig(config_obj *config_proto.Config) *oauth2.Config { - authenticator := config_obj.GUI.Authenticator - return &oauth2.Config{ - RedirectURL: config_obj.GUI.PublicUrl + "auth/azure/callback", - ClientID: authenticator.OauthClientId, - ClientSecret: authenticator.OauthClientSecret, - Scopes: []string{"User.Read"}, - Endpoint: microsoft.AzureADEndpoint(authenticator.Tenant), - } +func (self *AzureOidcRouter) LoginURL() string { + return api_utils.PublicURL(self.config_obj, self.LoginHandler()) } -func getUserDataFromAzure(ctx context.Context, - config_obj *config_proto.Config, code string) (*AzureUser, error) { +type AzureClaimsGetter struct { + config_obj *config_proto.Config + router OidcRouter +} - // Use code to get token and get user info from Azure. - azureOauthConfig := getAzureOauthConfig(config_obj) +func (self *AzureClaimsGetter) GetClaims( + ctx *HTTPClientContext, token *oauth2.Token) (claims *Claims, err error) { - token, err := azureOauthConfig.Exchange(ctx, code) - if err != nil { - return nil, fmt.Errorf("code exchange wrong: %s", err.Error()) + oauthConfig := &oauth2.Config{ + Endpoint: self.router.Endpoint(), } - response, err := azureOauthConfig.Client(ctx, token).Get( - "https://graph.microsoft.com/v1.0/me/") + client := oauthConfig.Client(ctx, token) + response, err := client.Get("https://graph.microsoft.com/v1.0/me/") if err != nil { - return nil, fmt.Errorf("failed getting user info: %s", err.Error()) + return nil, fmt.Errorf("failed getting user info: %v", err) } defer response.Body.Close() - contents, err := ioutil.ReadAll( - io.LimitReader(response.Body, constants.MAX_MEMORY)) - if err != nil { - return nil, fmt.Errorf("failed read response: %s", err.Error()) - } - - serialized, err := json.Marshal(token) + contents, err := utils.ReadAllWithLimit(response.Body, constants.MAX_MEMORY) if err != nil { - return nil, err + return nil, fmt.Errorf("failed read response: %v", err) } user_info := &AzureUser{} @@ -195,79 +110,30 @@ func getUserDataFromAzure(ctx context.Context, return nil, err } - // Store the oauth token in the JWT so that we can store it in - // the cookie. We will use the cookie value to retrieve the - // picture using some more Azure APIs. - user_info.Token = string(serialized) - - return user_info, nil -} - -// Get the token from the cookie and request the picture from Azure -func oauthAzurePicture(config_obj *config_proto.Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - reject := func(err error) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(http.StatusUnauthorized) - } - - auth_cookie, err := r.Cookie("VelociraptorAuth") - if err != nil { - reject(err) - return - } - - // Parse the JWT. - token, err := jwt.Parse( - auth_cookie.Value, - func(token *jwt.Token) (interface{}, error) { - _, ok := token.Method.(*jwt.SigningMethodHMAC) - if !ok { - return nil, errors.New("invalid signing method") - } - return []byte(config_obj.Frontend.PrivateKey), nil - }) - if err != nil { - reject(err) - return - } - - claims, ok := token.Claims.(jwt.MapClaims) - if !ok || !token.Valid { - reject(errors.New("token not valid")) - return - } - - // Record the username for handlers lower in the - // stack. - token_str, pres := claims["token"].(string) - if !pres { - reject(errors.New("token not present")) - return + username := user_info.Mail + if username != "" { + picture := self.getAzurePicture(client) + if picture != "" { + setUserPicture(ctx, username, picture) } + } - oauth_token := &oauth2.Token{} - err = json.Unmarshal([]byte(token_str), &oauth_token) - if err != nil { - reject(err) - return - } + return &Claims{ + Username: user_info.Mail, + }, nil +} - azureOauthConfig := getAzureOauthConfig(config_obj) - response, err := azureOauthConfig.Client(r.Context(), oauth_token).Get( - "https://graph.microsoft.com/v1.0/me/photos/48x48/$value") - if err != nil { - reject(fmt.Errorf("failed getting photo: %v", err)) - return - } - defer response.Body.Close() +// Best effort - if anything fails we just dont show the picture. +func (self *AzureClaimsGetter) getAzurePicture(client *http.Client) string { + response, err := client.Get("https://graph.microsoft.com/v1.0/me/photos/48x48/$value") + if err != nil { + return "" + } + defer response.Body.Close() - _, err = io.Copy(w, response.Body) - if err != nil { - reject(fmt.Errorf("failed getting photo: %v", err)) - return - } + data, _ := utils.ReadAllWithLimit(response.Body, + constants.MAX_MEMORY) - }) + return fmt.Sprintf("data:image/jpeg;base64,%v", + base64.StdEncoding.EncodeToString(data)) } diff --git a/api/authenticators/basic.go b/api/authenticators/basic.go index 31513801f..aee75bebf 100644 --- a/api/authenticators/basic.go +++ b/api/authenticators/basic.go @@ -4,42 +4,54 @@ import ( "context" "net/http" + "github.com/Velocidex/ordereddict" "github.com/gorilla/csrf" - "github.com/sirupsen/logrus" "www.velocidex.com/golang/velociraptor/acls" api_proto "www.velocidex.com/golang/velociraptor/api/proto" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/logging" - "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services" + utils "www.velocidex.com/golang/velociraptor/utils" ) // Implement basic authentication. -type BasicAuthenticator struct{} +type BasicAuthenticator struct { + config_obj *config_proto.Config +} // Basic auth does not need any special handlers. -func (self *BasicAuthenticator) AddHandlers(config_obj *config_proto.Config, mux *http.ServeMux) error { - base := config_obj.GUI.BasePath - mux.Handle(base+"/logoff", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - username, _, ok := r.BasicAuth() - if !ok { - w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - http.Error(w, "authorization failed", http.StatusUnauthorized) - return - } - - // The previous username is given as a query parameter. - params := r.URL.Query() - old_username, ok := params["username"] - if ok && len(old_username) == 1 && old_username[0] != username { - http.Redirect(w, r, base, http.StatusTemporaryRedirect) - return - } - - w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - http.Error(w, "authorization failed", http.StatusUnauthorized) - })) +func (self *BasicAuthenticator) AddHandlers(mux *api_utils.ServeMux) error { + return nil +} + +func (self *BasicAuthenticator) AddLogoff(mux *api_utils.ServeMux) error { + mux.Handle(api_utils.GetBasePath(self.config_obj, "/app/logoff.html"), + IpFilter(self.config_obj, + api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + username, _, ok := r.BasicAuth() + if !ok { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + http.Error(w, "authorization failed", http.StatusUnauthorized) + return + } + + // The previous username is given as a query parameter. + params := r.URL.Query() + old_username, ok := params["username"] + if ok && len(old_username) == 1 && old_username[0] != username { + // Authenticated as someone else. + http.Redirect(w, r, api_utils.Homepage(self.config_obj), + http.StatusTemporaryRedirect) + return + } + + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + http.Error(w, "authorization failed", http.StatusUnauthorized) + }))) return nil } @@ -48,73 +60,112 @@ func (self *BasicAuthenticator) IsPasswordLess() bool { return false } +func (self *BasicAuthenticator) RequireClientCerts() bool { + return false +} + +func (self *BasicAuthenticator) AuthRedirectTemplate() string { + return "" +} + func (self *BasicAuthenticator) AuthenticateUserHandler( - config_obj *config_proto.Config, - parent http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-CSRF-Token", csrf.Token(r)) - w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - - username, password, ok := r.BasicAuth() - if !ok { - http.Error(w, "Not authorized", http.StatusUnauthorized) - return - } - - // Get the full user record with hashes so we can - // verify it below. - user_record, err := users.GetUserWithHashes(config_obj, username) - if err != nil { - logger := logging.GetLogger(config_obj, &logging.Audit) - logger.WithFields(logrus.Fields{ - "username": username, - "status": http.StatusUnauthorized, - }).Error("Unknown username") - - http.Error(w, "authorization failed", http.StatusUnauthorized) - return - } - - // Must have at least reader. - perm, err := acls.CheckAccess(config_obj, username, acls.READ_RESULTS) - if !perm || err != nil || user_record.Locked || user_record.Name != username { - logger := logging.GetLogger(config_obj, &logging.Audit) - logger.WithFields(logrus.Fields{ - "username": username, - "status": http.StatusUnauthorized, - }).Error("Unauthorized username") - - http.Error(w, "authorization failed", http.StatusUnauthorized) - return - } - - if !users.VerifyPassword(user_record, password) { - logger := logging.GetLogger(config_obj, &logging.Audit) - logger.WithFields(logrus.Fields{ - "username": username, - "status": http.StatusUnauthorized, - }).Error("Invalid password") - - http.Error(w, "authorization failed", http.StatusUnauthorized) - return - } - - // Checking is successful - user authorized. Here we - // build a token to pass to the underlying GRPC - // service with metadata about the user. - user_info := &api_proto.VelociraptorUser{ - Name: username, - } - - // Must use json encoding because grpc can not handle - // binary data in metadata. - serialized, _ := json.Marshal(user_info) - ctx := context.WithValue( - r.Context(), constants.GRPC_USER_CONTEXT, string(serialized)) - - // Need to call logging after auth so it can access - // the USER value in the context. - GetLoggingHandler(config_obj)(parent).ServeHTTP( - w, r.WithContext(ctx)) - }) + parent http.Handler, + permission acls.ACL_PERMISSION, +) http.Handler { + + logger := GetLoggingHandler(self.config_obj)(parent) + + return api_utils.HandlerFunc(parent, + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-CSRF-Token", csrf.Token(r)) + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + + username, password, ok := r.BasicAuth() + if !ok { + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + // Get the full user record with hashes so we can + // verify it below. + users_manager := services.GetUserManager() + user_record, err := users_manager.GetUserWithHashes(r.Context(), + username, username) + if err != nil { + err := services.LogAudit(r.Context(), + self.config_obj, username, "Unknown username", + ordereddict.NewDict(). + Set("remote", r.RemoteAddr). + Set("status", http.StatusUnauthorized)) + if err != nil { + logger := logging.GetLogger(self.config_obj, &logging.FrontendComponent) + logger.Error("Unknown username %v %v", username, r.RemoteAddr) + } + http.Error(w, "authorization failed", http.StatusUnauthorized) + return + } + + ok, err = users_manager.VerifyPassword(r.Context(), + user_record.Name, user_record.Name, password) + if !ok || err != nil { + err := services.LogAudit(r.Context(), + self.config_obj, user_record.Name, "Invalid password", + ordereddict.NewDict(). + Set("remote", r.RemoteAddr). + Set("status", http.StatusUnauthorized)) + + // If we cant emit an audit log, log to regular logging. + if err != nil { + logger := logging.GetLogger(self.config_obj, &logging.FrontendComponent) + logger.Error("Invalid Password %v %v", user_record.Name, r.RemoteAddr) + } + + http.Error(w, "authorization failed", http.StatusUnauthorized) + return + } + + // Does the user have access to the specified org? + err = CheckOrgAccess(self.config_obj, r, user_record, permission) + if err != nil { + err1 := services.LogAudit(r.Context(), + self.config_obj, user_record.Name, "User Unauthorized for Org", + ordereddict.NewDict(). + Set("err", err.Error()). + Set("remote", r.RemoteAddr). + Set("status", http.StatusUnauthorized)) + if err1 != nil { + logger := logging.GetLogger(self.config_obj, &logging.FrontendComponent) + logger.Error("CheckOrgAccess LogAudit: User Unauthorized for Org %v %v", + user_record.Name, r.RemoteAddr) + } + + // Return status forbidden because we dont want the user + // to reauthenticate + http.Error(w, err.Error(), http.StatusForbidden) + return + } + + // Checking is successful - user authorized. Here we + // build a token to pass to the underlying GRPC + // service with metadata about the user. + user_info := &api_proto.VelociraptorUser{ + Name: user_record.Name, + } + + // Must use json encoding because grpc can not handle + // binary data in metadata. + serialized, _ := json.Marshal(user_info) + ctx := context.WithValue( + r.Context(), constants.GRPC_USER_CONTEXT, string(serialized)) + + _ = users_manager.SetUserStats(r.Context(), self.config_obj, username, + &api_proto.UserStats{ + LastActiveTime: utils.GetTime().Now().Unix(), + LastIpAddress: r.RemoteAddr, + }) + + // Need to call logging after auth so it can access + // the USER value in the context. + logger.ServeHTTP(w, r.WithContext(ctx)) + }).AddChild("GetLoggingHandler") } diff --git a/api/authenticators/certs.go b/api/authenticators/certs.go new file mode 100644 index 000000000..5c9c19a60 --- /dev/null +++ b/api/authenticators/certs.go @@ -0,0 +1,258 @@ +/* + An authenticator that uses client side certificates. + + WARNING: This authenticator is considered very experimental!!! There + are serious security considerations when using this so ensure you + understand all the ramifications before using it! + + This authenticator makes it possible to use distributed + authentication - if the user has the client certificate they will be + automatically authenticated! This is more risky than centralized + authentication because the security depends on the certificates + themselves. + + ## How to issue client certificates + + This authenticator uses the same certificates that are used in the + Velociraptor api. You can use the `config api_client` command to + generate new client certificates. + + velociraptor --config server.config.yaml config api_client --name Mike --pkcs12 mike.pkcs12 Mike.pem -v --password + + For convenience you can use the --pkcs12 flag to also save the + certificates in .pkcs12 format which can be imported into the + Windows trust store. It is recommended you use --password to armour + the certificates. + + ## Configuring the server for client certificates + + The server's authenticator can be configured by replacing the Basic + authenticator with the `Certs` authenticator under the GUI section. + + ``` + authenticator: + type: Certs + default_roles_for_unknown_user: + - reader + - administrator + ``` + + You can specify roles under `default_roles_for_unknown_user` which + allows the server to automatically create user accounts with these + roles when a client certificate is presented for an unknown + users. Be careful with this setting as it might allow anyone with a + valid signed certificate (even an API certificate) to elevate to + administrator. It is recommended the API *not* be used when using + this feature. + + ## How can I revoke a certificate? + + Currently certificates can not be revoked. Instead all the ACLs can + be removed from the user account which mean that user has no + access. You can not safely reuse the same user name once these + permissions are removed. + + ## Caveats + + It is not possible for clients to present an TLS client certificate + because they dont have one. Therefore the Frontend (the service + connecting to clients) can not require client certificates. Since + TLS requires client certificates *before* the HTTP headers it is + currently impossible to require client certificates **only** for the + GUI and not the frontend if they share the same port!!! + + This means that client certifacts do not work with using autocert + (in that case both frontend and GUI share the same port due to + limitations in the Let's Encrypt protocol). + + The server will refuse to start when the frontend is forced to use + client certificates. + +*/ + +package authenticators + +import ( + "context" + "crypto/x509" + "errors" + "fmt" + "net/http" + + "github.com/Velocidex/ordereddict" + "github.com/gorilla/csrf" + "www.velocidex.com/golang/velociraptor/acls" + acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" +) + +var ( + invalidCertError = errors.New("Invalid Client Certificate") +) + +// Certificate based authenticator. +type CertAuthenticator struct { + config_obj *config_proto.Config + x509_roots *x509.CertPool + default_roles []string +} + +// Cert auth does not need any special handlers. +func (self *CertAuthenticator) AddHandlers(mux *api_utils.ServeMux) error { + return nil +} + +// It is not really possible to log off when using client certs +func (self *CertAuthenticator) AddLogoff(mux *api_utils.ServeMux) error { + mux.Handle(api_utils.GetBasePath(self.config_obj, "/app/logoff.html"), + IpFilter(self.config_obj, + api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + http.Error(w, "authorization failed", http.StatusUnauthorized) + return + }))) + + return nil +} + +func (self *CertAuthenticator) IsPasswordLess() bool { + return true +} + +func (self *CertAuthenticator) RequireClientCerts() bool { + return true +} + +func (self *CertAuthenticator) AuthRedirectTemplate() string { + return "" +} + +func (self *CertAuthenticator) getUserNameFromTLSCerts(r *http.Request) (string, error) { + // We only trust certs issued by the Velociraptor CA. + x509_opts := x509.VerifyOptions{ + CurrentTime: utils.GetTime().Now(), + Roots: self.x509_roots, + } + + for _, cert := range r.TLS.PeerCertificates { + _, err := cert.Verify(x509_opts) + if err != nil { + continue + } + return cert.Subject.CommonName, nil + } + return "", invalidCertError +} + +func (self *CertAuthenticator) AuthenticateUserHandler( + parent http.Handler, + permission acls.ACL_PERMISSION, +) http.Handler { + + logger := GetLoggingHandler(self.config_obj)(parent) + + return api_utils.HandlerFunc(parent, + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-CSRF-Token", csrf.Token(r)) + + username, err := self.getUserNameFromTLSCerts(r) + if err != nil { + http.Error(w, + fmt.Sprintf("authorization failed: Client Certificate is not valid: %v", err), + http.StatusUnauthorized) + return + } + + users_manager := services.GetUserManager() + user_record, err := users_manager.GetUser(r.Context(), username, username) + if err != nil { + if utils.IsNotFound(err) || + len(self.default_roles) == 0 { + http.Error(w, + fmt.Sprintf("authorization failed for %v: %v", username, err), + http.StatusUnauthorized) + return + } + + // Create a new user role on the fly. + policy := &acl_proto.ApiClientACL{ + Roles: self.default_roles, + } + err := services.LogAudit(r.Context(), + self.config_obj, username, "Automatic User Creation", + ordereddict.NewDict(). + Set("roles", self.default_roles). + Set("remote", r.RemoteAddr)) + if err != nil { + logger := logging.GetLogger(self.config_obj, &logging.FrontendComponent) + logger.Error("GetUser LogAudit: Automatic User Creation %v %v", + username, r.RemoteAddr) + } + + // Use the super user principal to actually add the + // username so we have enough permissions. + err = users_manager.AddUserToOrg(r.Context(), services.AddNewUser, + utils.GetSuperuserName(self.config_obj), username, + []string{"root"}, policy) + if err != nil { + http.Error(w, + fmt.Sprintf("authorization failed: automatic user creation: %v", err), + http.StatusUnauthorized) + return + } + + user_record, err = users_manager.GetUser(r.Context(), username, username) + if err != nil { + http.Error(w, + fmt.Sprintf("Failed creating user for %v: %v", username, err), + http.StatusUnauthorized) + return + } + } + + // Does the user have access to the specified org? + err = CheckOrgAccess(self.config_obj, r, user_record, permission) + if err != nil { + err := services.LogAudit(r.Context(), + self.config_obj, user_record.Name, "Unauthorized username", + ordereddict.NewDict(). + Set("remote", r.RemoteAddr). + Set("status", http.StatusUnauthorized)) + if err != nil { + logger := logging.GetLogger(self.config_obj, &logging.FrontendComponent) + logger.Error("CheckOrgAccess LogAudit: Unauthorized username %v %v", + user_record.Name, r.RemoteAddr) + } + + http.Error(w, + fmt.Sprintf("authorization failed: %v", err), + http.StatusUnauthorized) + return + } + + // Checking is successful - user authorized. Here we + // build a token to pass to the underlying GRPC + // service with metadata about the user. + user_info := &api_proto.VelociraptorUser{ + Name: user_record.Name, + } + + // Must use json encoding because grpc can not handle + // binary data in metadata. + serialized, _ := json.Marshal(user_info) + ctx := context.WithValue( + r.Context(), constants.GRPC_USER_CONTEXT, string(serialized)) + + // Need to call logging after auth so it can access + // the USER value in the context. + logger.ServeHTTP(w, r.WithContext(ctx)) + }).AddChild("GetLoggingHandler") +} diff --git a/api/authenticators/claims.go b/api/authenticators/claims.go new file mode 100644 index 000000000..38a644ddb --- /dev/null +++ b/api/authenticators/claims.go @@ -0,0 +1,392 @@ +package authenticators + +import ( + "context" + "errors" + "fmt" + + "github.com/Velocidex/ordereddict" + oidc "github.com/coreos/go-oidc/v3/oidc" + jwt "github.com/golang-jwt/jwt/v4" + "golang.org/x/oauth2" + "www.velocidex.com/golang/velociraptor/acls" + acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" +) + +// The claims we care about - these are mapped from the IDP's claims +// using the oidc configuration. +type Claims struct { + Username string `json:"username"` + Picture string `json:"picture"` + Expires float64 `json:"expires"` + Token string `json:"token"` +} + +func (self *Claims) Valid() error { + if self.Username == "" { + return errors.New("username not present") + } + + if self.Expires < float64(utils.GetTime().Now().Unix()) { + return errors.New("the JWT is expired - reauthenticate") + } + return nil +} + +// A ClaimsGetter is responsible for fetching a claim from the oauth +// server. Depending on the server type the claims are encoded +// differently or fetched from different locations using different +// methods. +type ClaimsGetter interface { + GetClaims(ctx *HTTPClientContext, token *oauth2.Token) (*Claims, error) +} + +// The ClaimsGetter for standard OIDC endpoints. This fetches the +// claims from: +// 1. The standard OIDC UserInfo endpoint +// 2. Attempts to decode the claim from the AccessToken if it is a JWT. +// This behaviour was observed on ADFS. +type OidcClaimsGetter struct { + config_obj *config_proto.Config + authenticator *config_proto.Authenticator + router OidcRouter + + provider *oidc.Provider + + ignore_id_token bool +} + +func NewOidcClaimsGetter( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + authenticator *config_proto.Authenticator, + router OidcRouter) (*OidcClaimsGetter, error) { + + delegate, err := oidc.NewProvider(ctx, router.Issuer()) + if err != nil { + return nil, err + } + ep := delegate.Endpoint() + router.SetEndpoint(ep) + + return &OidcClaimsGetter{ + config_obj: config_obj, + authenticator: authenticator, + router: router, + provider: delegate, + }, nil +} + +func (self *OidcClaimsGetter) maybeGetClaimsFromToken( + ctx context.Context, token *oauth2.Token) (*ordereddict.Dict, error) { + + res := ordereddict.NewDict() + + // The AccessToken is supposed to be opaque since it is used as a + // bearer token by the API. We do not need to actually validate it. + + // On ADFS, this token can be decoded and actually contains some + // information about the user so we try to get that info anyway. + claims := jwt.MapClaims{} + _, _, err := jwt.NewParser().ParseUnverified(token.AccessToken, claims) + if err == nil { + for k, v := range claims { + res.Set(k, v) + } + } + + // Usually only used in tests - real IDPs should provide an ID + // token + if self.ignore_id_token { + return res, nil + } + + // The real claim is sent in the ID token + oidcConfig := &oidc.Config{ + ClientID: self.authenticator.OauthClientId, + } + + // https://github.com/Coreos/go-oidc/blob/v2.5.0/example/idtoken/app.go + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + return nil, errors.New("No ID Token") + } + + verifier := self.provider.Verifier(oidcConfig) + idToken, err := verifier.Verify(ctx, rawIDToken) + if err != nil { + return nil, err + } + + raw_id_token := make(map[string]interface{}) + err = idToken.Claims(&raw_id_token) + if err != nil { + return nil, err + } + + // Merge the ID token into the AccessToken + for k, v := range raw_id_token { + res.Set(k, v) + } + + return res, nil +} + +func (self *OidcClaimsGetter) UserInfo( + ctx context.Context, + token *oauth2.Token) (*oidc.UserInfo, error) { + user_info, err := self.provider.UserInfo( + ctx, oauth2.StaticTokenSource(token)) + if err != nil { + return nil, err + } + return user_info, err +} + +func (self *OidcClaimsGetter) Debug(message string, args ...interface{}) { + if self.authenticator.OidcDebug { + logging.GetLogger(self.config_obj, &logging.GUIComponent). + Debug(message, args...) + } +} + +func (self *OidcClaimsGetter) GetClaims( + ctx *HTTPClientContext, token *oauth2.Token) (claims *Claims, err error) { + + claims_dict, err := self.maybeGetClaimsFromToken(ctx, token) + if err == nil { + // Try to parse the claims from the token + res, err := self.newClaimsFromDict(ctx, self.config_obj, claims_dict) + if err == nil { + self.Debug("Unwrapped claims from AccessToken: %v", claims_dict) + return res, nil + } + + } else { + self.Debug("Unable to parse claims from tokens: %v", err) + } + + // If we cant get a valid claim from the token, we fallback to + // try using the user info method + claims_dict, err = self.getClaimsFromUserInfo(ctx, token) + if err != nil { + self.Debug("Unable to parse claims from user info: %v", err) + return nil, err + } + + res, err := self.newClaimsFromDict(ctx, self.config_obj, claims_dict) + if err != nil { + self.Debug("Unable to parse claims from user info claims dict: %v", err) + return nil, err + } + + self.Debug("Unwrapped claims from UserInfo: %v", claims_dict) + return res, nil +} + +func (self *OidcClaimsGetter) shouldRequireEmailVerify( + authenticator *config_proto.Authenticator) bool { + if authenticator.Claims == nil { + return true + } + + if authenticator.Claims.AllowUnverifiedEmail { + return false + } + + // If the user wants to use a different claim than email then + // email verified is not relevant. + if authenticator.Claims.Username != "" { + return false + } + + return true +} + +func (self *OidcClaimsGetter) getClaimsFromUserInfo( + ctx context.Context, token *oauth2.Token) (claims *ordereddict.Dict, err error) { + + user_info, err := self.UserInfo(ctx, token) + if err != nil { + return nil, fmt.Errorf("can not get UserInfo from OIDC provider: %v", err) + } + + // Make sure the user's email is verified because this is what we + // use as the identity. + if self.shouldRequireEmailVerify(self.authenticator) && + !user_info.EmailVerified { + return nil, fmt.Errorf("Email %v is not verified", user_info.Email) + } + + claims = ordereddict.NewDict() + err = user_info.Claims(&claims) + if err != nil { + return nil, err + } + + return claims, nil +} + +func (self *OidcClaimsGetter) newClaimsFromDict( + ctx context.Context, + config_obj *config_proto.Config, + claims *ordereddict.Dict) (*Claims, error) { + + username_field := "email" + if self.authenticator.Claims != nil && + self.authenticator.Claims.Username != "" { + + // Custom username field + username_field = self.authenticator.Claims.Username + self.Debug("Using field %v in claims for username", username_field) + } + + email, _ := claims.GetString(username_field) + if email == "" { + return nil, fmt.Errorf( + "OidcAuthenticator: Unable to parse name claim using field %v: %v", + username_field, claims) + } + + res := &Claims{ + Username: email, + } + + return res, self.SetRolesForUser( + ctx, config_obj, email, claims) +} + +func (self *OidcClaimsGetter) shouldUpdateACLs( + new_acl, existing_acls *acl_proto.ApiClientACL, +) (*acl_proto.ApiClientACL, bool) { + + // When OverrideAcls is specified we just replace the + // existing_acls with the new_acl if they are different. + if self.authenticator.Claims.OverrideAcls { + return new_acl, !acls.ACLEqual(new_acl, existing_acls) + } + + // Merge the old ACL with the new ACL + new_acl = acls.MergeACL(existing_acls, new_acl) + return new_acl, !acls.ACLEqual(new_acl, existing_acls) +} + +func (self *OidcClaimsGetter) SetRolesForUser( + ctx context.Context, + config_obj *config_proto.Config, + email string, + claims *ordereddict.Dict) error { + + // The roles field must be set to enable this feature! + var roles_field string + if self.authenticator.Claims != nil && + self.authenticator.Claims.Roles != "" { + roles_field = self.authenticator.Claims.Roles + + } else { + // Do nothing if automatic roles are not configured. + return nil + } + + // Do nothing if automatic roles are not configured. + roles, pres := claims.GetStrings(roles_field) + if !pres { + return nil + } + + user_manager := services.GetUserManager() + + logger := logging.GetLogger(config_obj, &logging.GUIComponent) + + // First check the user exist at all. + _, err := user_manager.GetUser(ctx, email, email) + if utils.IsNotFound(err) { + // If the user does not exist at all, create it. + user_record := &api_proto.VelociraptorUser{ + Name: email, + } + + err = services.LogAudit(ctx, config_obj, email, + "Create User From OIDC Roles", + ordereddict.NewDict().Set("Claims", claims)) + if err != nil { + return err + } + + err = user_manager.SetUser(ctx, user_record) + if err != nil { + return err + } + + // Some other error occured - reject. + } else if err != nil { + return err + } + + // Usually roles are set per org but setting roles through the + // OIDC IDP will grant the roles on all orgs. + org_manager, err := services.GetOrgManager() + if err != nil { + return err + } + + for _, org := range org_manager.ListOrgs() { + org_config_obj, err := org_manager.GetOrgConfig(org.Id) + if err != nil { + continue + } + + // Get the user's ACL policy in that org + existing_acls, err := services.GetPolicy(org_config_obj, email) + if err != nil { + // If a user does not exist this will fail to get their + // policy so start with a fresh policy. + existing_acls = &acl_proto.ApiClientACL{} + } + + new_acl := &acl_proto.ApiClientACL{} + + // For each role give by the IDP we assign velociraptor roles + for _, oidc_role := range roles { + acl_spec, pres := self.authenticator.Claims.RoleMap[oidc_role] + if !pres { + self.Debug("No allowed claim role map for OIDC claim %#v", oidc_role) + continue + } + + for _, role := range acl_spec.Roles { + if !utils.InString(new_acl.Roles, role) { + new_acl.Roles = append(new_acl.Roles, role) + } + } + } + + new_acl, should_update := self.shouldUpdateACLs(new_acl, existing_acls) + if should_update { + err = services.LogAudit(ctx, config_obj, email, + "Grant User Role From OIDC Claim", + ordereddict.NewDict(). + Set("ACL", new_acl). + Set("OrgId", org.Id). + Set("Claims", claims)) + if err != nil { + continue + } + + logger.Info("Granting acl %v to User %v in org %v", + json.MustMarshalString(new_acl), email, org.Id) + err = services.SetPolicy(org_config_obj, email, new_acl) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/api/authenticators/claims_test.go b/api/authenticators/claims_test.go new file mode 100644 index 000000000..dc68c4f61 --- /dev/null +++ b/api/authenticators/claims_test.go @@ -0,0 +1,558 @@ +package authenticators + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/Velocidex/ordereddict" + "github.com/stretchr/testify/suite" + "www.velocidex.com/golang/velociraptor/config" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/file_store/test_utils" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/services" + utils "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" +) + +// Replay responses to HTTP requests. Allows us to mock out the HTTP +// transactions. +type StaticURLReplayer struct { + name string + err_regex string + responses map[string]string + URLs []string + + authenticator *config_proto.Authenticator +} + +func (self *StaticURLReplayer) GetRoundTrip(rt RoundTripFunc) RoundTripFunc { + return func(req *http.Request) (*http.Response, error) { + resp, pres := self.responses[req.URL.String()] + if !pres { + fmt.Printf("Unknown request: %v\n", req.URL.String()) + } + resp = strings.TrimSpace(resp) + + result := &http.Response{ + StatusCode: 200, + Header: make(map[string][]string), + Body: io.NopCloser(bytes.NewReader([]byte(resp))), + } + + if strings.HasPrefix(resp, "{") { + result.Header["Content-Type"] = []string{ + "application/json; charset=utf-8"} + } else { + result.Header["Content-Type"] = []string{ + "application/x-www-form-urlencoded; charset=utf-8"} + } + + return result, nil + } +} + +var ( + testUrlReplayerCases = []StaticURLReplayer{ + StaticURLReplayer{ + name: "Typical Flow", + responses: map[string]string{ + // These are the responses for a typical oidc flow: + + // 1. First the provider queries the well known config endpoint. + `https://www.example.com/.well-known/openid-configuration`: ` +{"issuer": "https://www.example.com", + "authorization_endpoint": "https://www.example.com/o/oauth2/v2/auth", + "token_endpoint": "https://www.example.com/token", + "userinfo_endpoint": "https://www.example.com/v1/userinfo" +} +`, + // 2. Next we get the token + "https://www.example.com/token": ` +{"access_token": "This is an access token", + "expires_in": 3598, + "scope": "openid https://www.googleapis.com/auth/userinfo.email", + "token_type": "Bearer", + "id_token": "XXXX" +} +`, + // 3. Finally we fetch the user info endpoint. + "https://www.example.com/v1/userinfo": ` +{"sub": "100439259231459671911", + "email": "user@example.com", + "email_verified": true +} +`, + }, + }, + + StaticURLReplayer{ + // AWS Cognito does not follow the spec and encode + // email_verified as a string. We used to have special + // handling for it but it seems now the upstream library + // transparently fixed support. We test it anyway. + name: "AWS congnito", + responses: map[string]string{ + // These are the responses for a typical oidc flow: + + // 1. First the provider queries the well known config endpoint. + `https://www.example.com/.well-known/openid-configuration`: ` +{"issuer": "https://www.example.com", + "authorization_endpoint": "https://www.example.com/o/oauth2/v2/auth", + "token_endpoint": "https://www.example.com/token", + "userinfo_endpoint": "https://www.example.com/v1/userinfo" +} +`, + // 2. Next we get the token + "https://www.example.com/token": ` +{"access_token": "This is an access token", + "expires_in": 3598, + "scope": "openid https://www.googleapis.com/auth/userinfo.email", + "token_type": "Bearer", + "id_token": "XXXX" +} +`, + // 3. Finally we fetch the user info endpoint. + "https://www.example.com/v1/userinfo": ` +{"sub": "100439259231459671911", + "email": "user@example.com", + "email_verified": "true" +} +`, + }, + }, + + StaticURLReplayer{ + // ADFS seems to encode the user info in the AccessToken + // itself and returns a useless response to the UserInfo + // endpoint. We support this behavior implicitly. + name: "MS ADFS", + responses: map[string]string{ + `https://www.example.com/.well-known/openid-configuration`: ` +{"issuer": "https://www.example.com", + "authorization_endpoint": "https://www.example.com/o/oauth2/v2/auth", + "token_endpoint": "https://www.example.com/token", + "userinfo_endpoint": "https://www.example.com/v1/userinfo" +} +`, + // access_token contains a JWT with the email claim. + "https://www.example.com/token": ` +{"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBpZCI6IngxMjMiLCJlbWFpbCI6Im5vb25lQG5vd2hlcmUuY29tIiwiZXhwIjoxNzY0OTM3NDgzLCJzY3AiOiJlbWFpbCBvcGVuaWQifQ.3AjZi1Cxpbvscxh6HrDQ9iHOwom6G9RP-iRhv3vHLBo", + "expires_in": 3598, + "scope": "openid https://www.googleapis.com/auth/userinfo.email", + "token_type": "Bearer", + "id_token": "XXXX" +}`, + + // ADFS sends a minimal userinfo because most of the + // claims are encoded in the token. + "https://www.example.com/v1/userinfo": ` +{"sub": "100439259231459671911"}`, + }, + }, + + StaticURLReplayer{ + // Usually we require the email_verified claim to be able + // to use the email but some IDPs do not set it. We reject + // such uers. + name: "Email is not verified", + err_regex: "Email .+ is not verified", + responses: map[string]string{ + `https://www.example.com/.well-known/openid-configuration`: ` +{"issuer": "https://www.example.com", + "authorization_endpoint": "https://www.example.com/o/oauth2/v2/auth", + "token_endpoint": "https://www.example.com/token", + "userinfo_endpoint": "https://www.example.com/v1/userinfo" +} +`, + "https://www.example.com/token": ` +{"access_token": "This is an access token", + "expires_in": 3598, + "scope": "openid https://www.googleapis.com/auth/userinfo.email", + "token_type": "Bearer", + "id_token": "XXXX" +} +`, + "https://www.example.com/v1/userinfo": ` +{"sub": "100439259231459671911", + "email": "user@example.com", + "email_verified": false +} +`, + }, + }, + StaticURLReplayer{ + // Usually we require the email_verified claim to be able + // to use the email but some IDPs do not set it. We reject + // such uers. + name: "Email is not verified but it is allowed", + authenticator: &config_proto.Authenticator{ + Type: "oidc", + OauthClientId: "ClientIdXXXX", + OauthClientSecret: "ClientSecrect1234", + OidcIssuer: "https://www.example.com", + Claims: &config_proto.OIDCClaims{ + AllowUnverifiedEmail: true, + }, + }, + responses: map[string]string{ + `https://www.example.com/.well-known/openid-configuration`: ` +{"issuer": "https://www.example.com", + "authorization_endpoint": "https://www.example.com/o/oauth2/v2/auth", + "token_endpoint": "https://www.example.com/token", + "userinfo_endpoint": "https://www.example.com/v1/userinfo" +} +`, + "https://www.example.com/token": ` +{"access_token": "This is an access token", + "expires_in": 3598, + "scope": "openid https://www.googleapis.com/auth/userinfo.email", + "token_type": "Bearer", + "id_token": "XXXX" +} +`, + "https://www.example.com/v1/userinfo": ` +{"sub": "100439259231459671911", + "email": "user@example.com", + "email_verified": false +} +`, + }, + }, + StaticURLReplayer{ + // Usually we use the email claim but some IDPs use other + // claims to identify the user. + name: "Unusual claim name", + authenticator: &config_proto.Authenticator{ + Type: "oidc", + OauthClientId: "ClientIdXXXX", + OauthClientSecret: "ClientSecrect1234", + OidcIssuer: "https://www.example.com", + Claims: &config_proto.OIDCClaims{ + Username: "SomeWeirdClaim", + }, + }, + responses: map[string]string{ + `https://www.example.com/.well-known/openid-configuration`: ` +{"issuer": "https://www.example.com", + "authorization_endpoint": "https://www.example.com/o/oauth2/v2/auth", + "token_endpoint": "https://www.example.com/token", + "userinfo_endpoint": "https://www.example.com/v1/userinfo" +} +`, + "https://www.example.com/token": ` +{"access_token": "This is an access token", + "expires_in": 3598, + "scope": "openid https://www.googleapis.com/auth/userinfo.email", + "token_type": "Bearer" +} +`, + "https://www.example.com/v1/userinfo": ` +{"sub": "100439259231459671911", + "SomeWeirdClaim": "user@example.com" +} +`, + }, + }, + StaticURLReplayer{ + name: "Google Authenticator", + authenticator: &config_proto.Authenticator{ + Type: "google", + OauthClientId: "ClientIdXXXX", + OauthClientSecret: "ClientSecrect1234", + }, + responses: map[string]string{ + `https://accounts.google.com/.well-known/openid-configuration`: ` +{ + "issuer": "https://accounts.google.com", + "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "device_authorization_endpoint": "https://oauth2.googleapis.com/device/code", + "token_endpoint": "https://oauth2.googleapis.com/token", + "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo", + "revocation_endpoint": "https://oauth2.googleapis.com/revoke", + "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs" +} +`, + "https://oauth2.googleapis.com/token": ` +{"access_token": "This is an access token", + "expires_in": 3598, + "scope": "openid https://www.googleapis.com/auth/userinfo.email", + "token_type": "Bearer" +} +`, + "https://openidconnect.googleapis.com/v1/userinfo": ` +{"sub": "100439259231459671911", + "picture": "https://lh3.googleusercontent.com/a-/XXXXXX", + "email": "user@example.com", + "email_verified": true, + "hd": "example.com" +} +`, + }, + }, + StaticURLReplayer{ + name: "Github Authenticator", + authenticator: &config_proto.Authenticator{ + Type: "github", + OauthClientId: "ClientIdXXXX", + OauthClientSecret: "ClientSecrect1234", + }, + responses: map[string]string{ + "https://github.com/login/oauth/access_token": `access_token=XXXX&scope=user%3Aemail&token_type=bearer`, + "https://api.github.com/user": ` +{"login":"gh_user", + "id":12345, + "node_id":"XXXX=", + "avatar_url":"https://avatars.githubusercontent.com/u/3856546?v=4", + "gravatar_id":"","url":"https://api.github.com/users/gh_user", + "html_url":"https://github.com/gh_user", + "followers_url":"https://api.github.com/users/gh_user/followers", + "following_url":"https://api.github.com/users/gh_user/following{/other_user}", + "gists_url":"https://api.github.com/users/gh_user/gists{/gist_id}", + "starred_url":"https://api.github.com/users/gh_user/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/gh_user/subscriptions", + "organizations_url":"https://api.github.com/users/gh_user/orgs", + "repos_url":"https://api.github.com/users/gh_user/repos", + "events_url":"https://api.github.com/users/gh_user/events{/privacy}", + "received_events_url":"https://api.github.com/users/gh_user/received_events", + "type":"User", + "user_view_type":"public", + "site_admin":false, + "name":"Mike Cohen", + "company":"@Velocidex ", + "blog":"", + "location":"Australia" +} +`, + }, + }, + StaticURLReplayer{ + name: "Azure Authenticator", + authenticator: &config_proto.Authenticator{ + Type: "azure", + OauthClientId: "ClientIdXXXX", + OauthClientSecret: "ClientSecrect1234", + Tenant: "F1234", + }, + responses: map[string]string{ + "https://login.microsoftonline.com/F1234/oauth2/v2.0/token": ` +{"token_type":"Bearer", + "scope":"profile User.Read openid email", + "expires_in":4471, + "ext_expires_in":4471, + "access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBpZCI6IngxMjMiLCJlbWFpbCI6Im5vb25lQG5vd2hlcmUuY29tIiwiZXhwIjoxNzY0OTM3NDgzLCJzY3AiOiJlbWFpbCBvcGVuaWQifQ.3AjZi1Cxpbvscxh6HrDQ9iHOwom6G9RP-iRhv3vHLBo" +}`, + "https://graph.microsoft.com/v1.0/me/": ` +{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#users/$entity", + "businessPhones":["0470238491"], + "displayName":"Mike Cohen", + "givenName":"Mike", + "jobTitle":null, + "mail":"user@example.com", + "mobilePhone":null, + "officeLocation":null, + "preferredLanguage":"en-US", + "surname":"Cohen", + "userPrincipalName":"user@example.com", + "id":"bcb8e2a7-b2d6-49e1-a6c0-327fc8ea1058"} +`, + "https://graph.microsoft.com/v1.0/me/photos/48x48/$value": `My Picture`, + }, + }, + } +) + +type TestRouter struct { + DefaultOidcRouter +} + +func (self *TestRouter) Issuer() string { + return "https://www.example.com" +} + +type OauthTestSuire struct { + test_utils.TestSuite +} + +func (self *OauthTestSuire) TestAutoRoleCreation() { + t := self.T() + + ctx, err := ClientContext( + context.Background(), self.ConfigObj, + []Transformer{testUrlReplayerCases[0].GetRoundTrip}) + assert.NoError(t, err) + + authenticator := &config_proto.Authenticator{ + Type: "oidc", + OauthClientId: "ClientIdXXXX", + OauthClientSecret: "ClientSecrect1234", + OidcIssuer: "https://www.example.com", + Claims: &config_proto.OIDCClaims{ + Roles: "roles", + RoleMap: map[string]*config_proto.OIDCACL{ + "OIDCReader": &config_proto.OIDCACL{ + Roles: []string{"reader"}, + }, + }, + }, + } + + auther, err := getAuthenticatorByType(ctx, self.ConfigObj, authenticator) + assert.NoError(t, err) + + email := "user1" + + user_manager := services.GetUserManager() + _, err = user_manager.GetUser(self.Ctx, email, email) + + // User should not exist + assert.Error(t, err) + + oidc_auther, ok := auther.(*OidcAuthenticator) + assert.True(t, ok) + + claims_getter, ok := oidc_auther.claims_getter.(*OidcClaimsGetter) + assert.True(t, ok) + + claims := ordereddict.NewDict(). + Set("email", email). + Set("roles", []string{"OIDCReader"}) + + _, err = claims_getter.newClaimsFromDict( + self.Ctx, self.ConfigObj, claims) + assert.NoError(t, err) + + policy, err := services.GetPolicy(self.ConfigObj, email) + assert.NoError(t, err) + + g := ordereddict.NewDict() + g.Set("New Reader User", policy) + + // Now give the user more permissions - make them into an admin + err = services.GrantRoles(self.ConfigObj, email, []string{"administrator"}) + assert.NoError(t, err) + + policy, err = services.GetPolicy(self.ConfigObj, email) + assert.NoError(t, err) + + g.Set("Promote User to Admin", policy) + + // Assign the OIDC roles again + _, err = claims_getter.newClaimsFromDict( + self.Ctx, self.ConfigObj, claims) + assert.NoError(t, err) + + policy, err = services.GetPolicy(self.ConfigObj, email) + assert.NoError(t, err) + + g.Set("OIDC Adds roles to user", policy) + + // By default OIDC roles are additive. However, this means it is + // impossible to remove roles from users via OIDC which is a + // common use case. To force ACLs to be removed from the user to + // comply with the OIDC policy exactly we need to set the + // OverrideAcls flag. + authenticator.Claims.OverrideAcls = true + + _, err = claims_getter.newClaimsFromDict( + self.Ctx, self.ConfigObj, claims) + assert.NoError(t, err) + + policy, err = services.GetPolicy(self.ConfigObj, email) + assert.NoError(t, err) + + g.Set("OIDC removes roles from user", policy) + + // Now completely remove access via OIDC roles + claims.Set("roles", []string{}) + + _, err = claims_getter.newClaimsFromDict( + self.Ctx, self.ConfigObj, claims) + assert.NoError(t, err) + + policy, err = services.GetPolicy(self.ConfigObj, email) + assert.NoError(t, err) + + g.Set("OIDC removes all roles from user", policy) + + goldie.Assert(t, "TestAutoRoleCreation", json.MustMarshalIndent(g)) +} + +func (self *OauthTestSuire) TestProvider() { + closer := utils.MockTime(utils.NewMockClock(time.Unix(1765349444, 0))) + defer closer() + + t := self.T() + + config_obj := config.GetDefaultConfig() + golden := ordereddict.NewDict() + + for _, tc := range testUrlReplayerCases { + if false && tc.name != "Azure Authenticator" { + continue + } + + g := ordereddict.NewDict() + golden.Set(tc.name, g) + + authenticator := tc.authenticator + if authenticator == nil { + authenticator = &config_proto.Authenticator{ + Type: "oidc", + OauthClientId: "ClientIdXXXX", + OauthClientSecret: "ClientSecrect1234", + OidcIssuer: "https://www.example.com", + } + } + + ctx, err := ClientContext( + context.Background(), config_obj, + []Transformer{tc.GetRoundTrip}) + assert.NoError(t, err) + + auther, err := getAuthenticatorByType(ctx, config_obj, authenticator) + assert.NoError(t, err) + + oidc_auther, ok := auther.(*OidcAuthenticator) + assert.True(t, ok) + + claims_getter, ok := oidc_auther.claims_getter.(*OidcClaimsGetter) + if ok { + // Set by this test so we dont have to have a real ID + // token. In reality the claims_getter will verify the ID + // token with the IDP. + claims_getter.ignore_id_token = true + } + + provider, err := oidc_auther.Provider() + assert.NoError(t, err) + + // See the redirect URL. + redirect := provider.GetRedirectURL(nil, "StateString") + g.Set("Redirect URL", redirect) + + cookie, claims, err := provider.GetJWT(ctx, "code") + if tc.err_regex != "" { + assert.Regexp(t, tc.err_regex, err.Error()) + continue + } + assert.NoError(t, err) + cookie.Value = fmt.Sprintf("String of length %v", len(cookie.Value)) + + g.Set("Cookie", cookie) + g.Set("Claims", claims) + } + + goldie.Assert(t, "TestProvider", json.MustMarshalIndent(golden)) +} + +func TestOIDC(t *testing.T) { + suite.Run(t, &OauthTestSuire{}) +} diff --git a/api/authenticators/common.go b/api/authenticators/common.go new file mode 100644 index 000000000..6d81e3b35 --- /dev/null +++ b/api/authenticators/common.go @@ -0,0 +1 @@ +package authenticators diff --git a/api/authenticators/fixtures/TestAutoRoleCreation.golden b/api/authenticators/fixtures/TestAutoRoleCreation.golden new file mode 100644 index 000000000..103bf42d2 --- /dev/null +++ b/api/authenticators/fixtures/TestAutoRoleCreation.golden @@ -0,0 +1,24 @@ +{ + "New Reader User": { + "roles": [ + "reader" + ] + }, + "Promote User to Admin": { + "roles": [ + "administrator" + ] + }, + "OIDC Adds roles to user": { + "roles": [ + "administrator", + "reader" + ] + }, + "OIDC removes roles from user": { + "roles": [ + "reader" + ] + }, + "OIDC removes all roles from user": {} +} \ No newline at end of file diff --git a/api/authenticators/fixtures/TestProvider.golden b/api/authenticators/fixtures/TestProvider.golden new file mode 100644 index 000000000..9c431b862 --- /dev/null +++ b/api/authenticators/fixtures/TestProvider.golden @@ -0,0 +1,205 @@ +{ + "Typical Flow": { + "Redirect URL": "https://www.example.com/o/oauth2/v2/auth?client_id=ClientIdXXXX\u0026redirect_uri=https%3A%2F%2Flocalhost%3A8889%2Fauth%2Foidc%2Fcallback\u0026response_type=code\u0026scope=openid+email\u0026state=StateString", + "Cookie": { + "Name": "VelociraptorAuth", + "Value": "String of length 183", + "Quoted": false, + "Path": "/", + "Domain": "", + "Expires": "2025-12-11T06:50:44Z", + "RawExpires": "", + "MaxAge": 0, + "Secure": true, + "HttpOnly": true, + "SameSite": 0, + "Partitioned": false, + "Raw": "", + "Unparsed": null + }, + "Claims": { + "username": "user@example.com", + "picture": "", + "expires": 1765435844, + "token": "" + } + }, + "AWS congnito": { + "Redirect URL": "https://www.example.com/o/oauth2/v2/auth?client_id=ClientIdXXXX\u0026redirect_uri=https%3A%2F%2Flocalhost%3A8889%2Fauth%2Foidc%2Fcallback\u0026response_type=code\u0026scope=openid+email\u0026state=StateString", + "Cookie": { + "Name": "VelociraptorAuth", + "Value": "String of length 183", + "Quoted": false, + "Path": "/", + "Domain": "", + "Expires": "2025-12-11T06:50:44Z", + "RawExpires": "", + "MaxAge": 0, + "Secure": true, + "HttpOnly": true, + "SameSite": 0, + "Partitioned": false, + "Raw": "", + "Unparsed": null + }, + "Claims": { + "username": "user@example.com", + "picture": "", + "expires": 1765435844, + "token": "" + } + }, + "MS ADFS": { + "Redirect URL": "https://www.example.com/o/oauth2/v2/auth?client_id=ClientIdXXXX\u0026redirect_uri=https%3A%2F%2Flocalhost%3A8889%2Fauth%2Foidc%2Fcallback\u0026response_type=code\u0026scope=openid+email\u0026state=StateString", + "Cookie": { + "Name": "VelociraptorAuth", + "Value": "String of length 184", + "Quoted": false, + "Path": "/", + "Domain": "", + "Expires": "2025-12-11T06:50:44Z", + "RawExpires": "", + "MaxAge": 0, + "Secure": true, + "HttpOnly": true, + "SameSite": 0, + "Partitioned": false, + "Raw": "", + "Unparsed": null + }, + "Claims": { + "username": "noone@nowhere.com", + "picture": "", + "expires": 1765435844, + "token": "" + } + }, + "Email is not verified": { + "Redirect URL": "https://www.example.com/o/oauth2/v2/auth?client_id=ClientIdXXXX\u0026redirect_uri=https%3A%2F%2Flocalhost%3A8889%2Fauth%2Foidc%2Fcallback\u0026response_type=code\u0026scope=openid+email\u0026state=StateString" + }, + "Email is not verified but it is allowed": { + "Redirect URL": "https://www.example.com/o/oauth2/v2/auth?client_id=ClientIdXXXX\u0026redirect_uri=https%3A%2F%2Flocalhost%3A8889%2Fauth%2Foidc%2Fcallback\u0026response_type=code\u0026scope=openid+email\u0026state=StateString", + "Cookie": { + "Name": "VelociraptorAuth", + "Value": "String of length 183", + "Quoted": false, + "Path": "/", + "Domain": "", + "Expires": "2025-12-11T06:50:44Z", + "RawExpires": "", + "MaxAge": 0, + "Secure": true, + "HttpOnly": true, + "SameSite": 0, + "Partitioned": false, + "Raw": "", + "Unparsed": null + }, + "Claims": { + "username": "user@example.com", + "picture": "", + "expires": 1765435844, + "token": "" + } + }, + "Unusual claim name": { + "Redirect URL": "https://www.example.com/o/oauth2/v2/auth?client_id=ClientIdXXXX\u0026redirect_uri=https%3A%2F%2Flocalhost%3A8889%2Fauth%2Foidc%2Fcallback\u0026response_type=code\u0026scope=openid+email\u0026state=StateString", + "Cookie": { + "Name": "VelociraptorAuth", + "Value": "String of length 183", + "Quoted": false, + "Path": "/", + "Domain": "", + "Expires": "2025-12-11T06:50:44Z", + "RawExpires": "", + "MaxAge": 0, + "Secure": true, + "HttpOnly": true, + "SameSite": 0, + "Partitioned": false, + "Raw": "", + "Unparsed": null + }, + "Claims": { + "username": "user@example.com", + "picture": "", + "expires": 1765435844, + "token": "" + } + }, + "Google Authenticator": { + "Redirect URL": "https://accounts.google.com/o/oauth2/auth?client_id=ClientIdXXXX\u0026redirect_uri=https%3A%2F%2Flocalhost%3A8889%2Fauth%2Fgoogle%2Fcallback\u0026response_type=code\u0026scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email\u0026state=StateString", + "Cookie": { + "Name": "VelociraptorAuth", + "Value": "String of length 183", + "Quoted": false, + "Path": "/", + "Domain": "", + "Expires": "2025-12-11T06:50:44Z", + "RawExpires": "", + "MaxAge": 0, + "Secure": true, + "HttpOnly": true, + "SameSite": 0, + "Partitioned": false, + "Raw": "", + "Unparsed": null + }, + "Claims": { + "username": "user@example.com", + "picture": "", + "expires": 1765435844, + "token": "" + } + }, + "Github Authenticator": { + "Redirect URL": "https://github.com/login/oauth/authorize?client_id=ClientIdXXXX\u0026redirect_uri=https%3A%2F%2Flocalhost%3A8889%2Fauth%2Fgithub%2Fcallback\u0026response_type=code\u0026scope=user%3Aemail\u0026state=StateString", + "Cookie": { + "Name": "VelociraptorAuth", + "Value": "String of length 171", + "Quoted": false, + "Path": "/", + "Domain": "", + "Expires": "2025-12-11T06:50:44Z", + "RawExpires": "", + "MaxAge": 0, + "Secure": true, + "HttpOnly": true, + "SameSite": 0, + "Partitioned": false, + "Raw": "", + "Unparsed": null + }, + "Claims": { + "username": "gh_user", + "picture": "", + "expires": 1765435844, + "token": "" + } + }, + "Azure Authenticator": { + "Redirect URL": "https://login.microsoftonline.com/F1234/oauth2/v2.0/authorize?client_id=ClientIdXXXX\u0026redirect_uri=https%3A%2F%2Flocalhost%3A8889%2Fauth%2Fazure%2Fcallback\u0026response_type=code\u0026scope=User.Read\u0026state=StateString", + "Cookie": { + "Name": "VelociraptorAuth", + "Value": "String of length 183", + "Quoted": false, + "Path": "/", + "Domain": "", + "Expires": "2025-12-11T06:50:44Z", + "RawExpires": "", + "MaxAge": 0, + "Secure": true, + "HttpOnly": true, + "SameSite": 0, + "Partitioned": false, + "Raw": "", + "Unparsed": null + }, + "Claims": { + "username": "user@example.com", + "picture": "", + "expires": 1765435844, + "token": "" + } + } +} \ No newline at end of file diff --git a/api/authenticators/github.go b/api/authenticators/github.go index 4062928fc..0348e90c3 100644 --- a/api/authenticators/github.go +++ b/api/authenticators/github.go @@ -1,190 +1,118 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package authenticators import ( "fmt" - "io" - "io/ioutil" - "net/http" - "time" - - jwt "github.com/golang-jwt/jwt" - "github.com/sirupsen/logrus" - context "golang.org/x/net/context" + "golang.org/x/oauth2" "golang.org/x/oauth2/github" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/json" - "www.velocidex.com/golang/velociraptor/logging" + utils "www.velocidex.com/golang/velociraptor/utils" ) -type GitHubUser struct { - Login string `json:"login"` - AvatarUrl string `json:"avatar_url"` +const ( + GithubIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPAAAADwCAYAAAA+VemSAAAACXBIWXMAADddAAA3XQEZgEZdAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABiOSURBVHgB7Z2/cxtHlsffG0BnytqqxdpeexWZqiXvSO+WCGWXCcw2s5ztRaKyyyRll4nMLpP0F4jKLhMZXiQou8xDl21qTboMZ7pbn01XnS3aBKavXxOwYRk/Zgbzo9/M91NFE6IJkQL62+9Hv36PCAAAAAAAAAAAAAAAAAAAAIBfwQRUsrbWXh40qGWYWmxoefR1M/Z4EszUk89RRCfcsB+GThoDOnn+POwRUAcE7CHLy+3WhUtOiG1DIlCzYYXZYvmzFSzZx5QHTCf27+7ZRyd2KwiJg2/tzw2DgHpHn4QhAe+AgEtGxNq4SB0OrGAjc53PRbtMfiIi7lkzfiDCvmA/YLnLBQIumNU/tUWgHbGq1tp1PBZrLOwC6hkRdsDP7OMuLHWxQMA5I7Fqv0E3xLraV7uTm/vrC+dueJcD3rexdRcWOl8g4BxY+XO7Q4PofSa+od3CLsq5hTZ71Aj2jz8OuwQyBQLOiJFobXy4VXkrmxJ28TN1TcCPIeZsgIAXwIk2iq7bl/EORJuMkZgbEe/AzU4PBJwQyRo3L9GWjWnft3/sEMiC0MbMD8++o71eLzwhEBsIOCYuexxFN+Ei54hNgNkFuQerHB8IeA7nsa25R7C2RdOlBu8gVp4NBDwFCNcPXKwc8I49X94l8Csg4FewrrLEt/fqfvzjGxDyZCDgIWJxeWAeQbh+AyH/ktoLGK6yTlyBSINv1T1Grq2AXYkjm0cE4aqGmXbrnLWunYDdOe7r0W0yvE2gOrDZ7n8fPKzbOXKDasQf19s3mhfME/tu3yBQMbjTuEB/ffOdy99+/fcXtbkRVQsLDHe5XtTJra68BV5dv3o7Cug/7MM1AnWhHTFtvfH7yz98/dWL/6IKU1kLDKsLhnSbhm9V1RpX0gL/49rVm4OA9ghWF9i8pWG6UdXYuFIW2GWYL0b33PU+AF4lMA/63wU7VcpUV0bAcluIIvMElVRgFlIA0jC8WRWXuhIutCSqrHB37cM/EACzaVUpwaVewCvrV+/bfXXbPlwiAOIha+Uvb7zzTuvrv//3f5Ji1LrQwyzzE/uwTQCkJ7RZ6g+0utQqBezGirB5ingXZIHmuDggZcjtoX5gPoR4QVbIWrLe3IdSakvKUBUDS7LKvtpSVYV4F2TNkrXEf7XJrW81JbfUCHhl7eo9Yv53AiBf/vLG2++QTW49IwWoEPBQvNsEQCFwR4uIvRewOyZi/jcCoFB0iNhrAa++t/HIvpD/SgCUAnfefPsPy1bE++Qp3gpYxGsMbREA5dL2WcReCnhYXQXLC3zBWxF7J+BhwgoxL/CNto8xsVcCRrYZ+I1/iS1vBAzxAh34JWIvBCwdNAzzAwJABdx5663Lvf/96sUBlUzplxnOx3aaDwkAXZxwwJtHn4Sltukp9TKD3CqSLhoEgD5asnbdGi6R0iyw9K+6cBG3ioBu5Cri2Uu+VlafrdIscPN1TAIE+pE13Lzo2heXQilJLJdxRqEGqA5rZWWmC3ehh+M8nxIAVaPBm0WPOy1UwGiFAyrOSdPwtSJb8zSpQIajTpapTNim/w3/lHAwbFo2kGkR0MfwvTT2s30PT1imMJBZpvJoDdf4JhVEYRbYj0or7h4fhr96cd1EhyVq21dDPq6zMW14CZ7hRMqhFeozK9jwgqFwkqVbWbMhGpcdovFdu84KKUwqRMDDFrBfUMnY3frW0fNwN873SoFJFNFWQOZ9iLkkxMIS75mIHvdPKYx7VLOyviEC7lCZFBQP5y5gn857+y/5d2nO687HttAd626/D3e7CLjLAT0++4720rxfK+vtOzY4uk8lUtT5cO4CHt7t9WDYGO9bt2ahtqFuM1qiG9aU34NVzhhrbY3hx2xo7/j5YpZruW1Doh/MN1Q2gXlw/MlHdylHchWwT0dGSdznOKyutbcg5AyQ2Dbih9ZFfpCltfLCjRZydqVzrcTiQXkVKq/SIOpShshmcHR4cEU2BnGXCCTjPCm10/+er1iLu525q2nYi+t+ogHx3CgnchOwZJ29sU48OWOZBRBycph5txnxtVyE+zNd8gBXavk65RZC5uJC+5J1HmGIH35+GBYSh9tjjG0b+9xGsmsS3LWvy86iMW4cXBz8o12DnrwPTcNX8jAiuRRy9AObATTkE10qCLEqdgPbHTBtGzI3KSPscUpPzj/tjntiX9ovXeFCYB8PqMcNOmkMyFmy01M6mWXVxJ1bWqLWoEEt+1y3uO3zl41xHy1metctejbtzBa/HAdFfDfLHMQ8emF4svLeRo88mV6ZV4FH5hbYHrlsmcif2FfggK+VcfFahmXZc+T7sUMJiQutUO2bEloxHdjn9aYVLBSBy7pfomV7Hr7M7ISwIV8mY2KLQryfwUvaLuO6nX39HzBZb8gT7GvxgfUE9yhDshfw+sYXvmVmjw8PSu084txqm7F+9etiVe1/uvZDEi5h2d0d4vJT5Vpgs7yGrk+y1pIPsMdCt4pwl6fhw3nwOHmcDWfqQnuVuBph3U4qmZFb3Q/oibWsz4KAwrRFCj4w/L27NBaanLdGcsc2Url20C/J6o4jHkzpPaPGGEtobVNGZPbv8/em0eT6Z1B9fEumDjnpv+QrWW1umR0jDQI/ixps7NYjUEtOl8hHD6eV5bFSJgKWnc7XOUYuYwtqiWSiyUeMuZ1VM7xMBCzWlwDwEE+La1pZaWZhAftsfQHwFdFMFlZ4YQHD+gKQjjNePBZeSMCwvgCkh8ncXPSiw0IChvUFYCEWzkinFrDz340H9y3nYHCpoNZ4f1/bZqQXscKpBTxoUEfDZfaA6bcEaoncSCL/WcgKp3ehIx3us6H4hfegWlz4Tke3FDbpb62lErDcstHSSobhQteWqKljjYqWXPupFKQSMGd4zzVv5MXJs6UJ8JfhFUgdDNJ5tIkFPDx8vkGKaP5G0RsJssOYDdJDJ42hSSzgPusSryPyP1sOcoB1ve9pklmJBexTh4PYGHOdQK1YXW1n1xKoINIksxIJWObOKO2D3EEcXC9MU5/XlSaZlUjAHOhJXr3KhUsKXX+QHta5Vs0g2TpN5kIrqLyaivRuArXAFXAYnYnLpCc8sQWs2H12d0IbEe0QqAVykV8a7ZNOWknc6NgC1uo+O/Ea3iyrNSsoB9eD2rDKTTuJGx3fhVbqPkcDvgvx1hPpBkqejFhJQhI3OpaAJSWv0n1m3vn8s2wbaQNd9F/jDxTOrIrtRscSsLmgL4Mrb9rxp24HBjVG4mGjMR6OWXwUz4VWWAghcS8BQM6V7sqIF9JETM3NFfDwTmWHNGFdZ8S9YJzBa7StzJWOVXw0V8CNH3SJV96k/vf0gAAYQ1zpiPguKaJxcb724rjQHdKEPTrQOnMI5MtwMmCX9NCZ9w1zBcysJ/4V61vkDFqgEEVnw3G0N1PA6krSlB7cg+IYjjvtkgas9ubFwTMF3DzVI15YXxAbRRv9vGYUs13oQFH8C+sLYiJWWE1GerCIgBWd/zYUlsyB8jCGH5MOOrP+52wBq2kKxvs49wVJ6C/pOGpkmt3Xa6qAhwksFV0s2BDqnUEihrODu+Q587qqThWwpgTW2SkEDNLA+6SAWQUd011oPe5zF4UbIA1NJZ4bz7gJOFXAiq4PqthFgX9I3kRDNpqZpsbBUwU8L3j2hgaFBEBq+Bl5zqz5Xupd6OOPXWUNAOkI/E9kzZrvNV3AKjLQ3CUAFuFH/z24WZnoiQJ2Xe0VYP9hBwTAApxd0lGRtbRE8QVsGkrOf/X1OgKe4VrQKlhHZ1NC2okC5oaauao9AmBBNKyjIEhigY2S4d0NwvkvWBgm/pI8Z5ompwlYhQvd7MMCg8WJyH9DME2TwZQvqhDw6SksMFgcZv/XUcD024lfn/RFe3D8LikAJZQgE4wKC7w86euJB3wDUDU4UODJsYnvQrPSKYQAVBVOEgMDAHQwOQZmHUksAOqCoSQWWMkxEgBZoKTuoXoudJzZMQBUGdUCnlbgDUBdQBIL1B4tlYeTUC3gfhPHXWBxtFQeTkK1gM0ALjRYHC2Vh5NQLeBpV6wASITiY9PJAlZQ3C1oufYIvGeZ/GeiJieXUioo7haYSK3rAzxCQRKLkwhYC9YC/44AWAAt/d+mMa2UUokFVtK7GniLlv5vhrg36evTSil1xMBzBj8BMBc1I4QmM+U6of89gkZcuIREFkgPsw4vjnly+6iJAtbQI2gM1TsoKBctJxmRoW8nfX2yBVYSAwtRBAGDheiQAqZpcpqAe6QE6wJdJwBSsLLW7pASkrnQkSIX2lAbiSyQCkUJLJNEwBeMrpGdzd/AjQZp0OO9TSuumijg0yVd/ZbNgG4QAElhHfGvcPRJONGoThSwloFPIxAHg6S4+FfPPeCpBnVqKaWWaiwH4mCQELu+FXltPDWknSpgNqxq9m7zIm0RADEJyLxPSpg1BzuY8SRViSxS9IaAchH32SgaXjArnJ0l4B7porP6J903S0AxcGBukiYalNyFHrxGXVKGMchGgxgYPdlnof9/KQQsmWgtnTl+wpjbSGaBWVgvbUub+zxrCufsC/1GnRVuNV+nOwTANCJzjxRhaHYyeY6AdWWiBTbK4htQGNqsr4NnJ5PntdTpkjLkDZI3igB4FWXW1xHM1uBMAfeXtB0lnWMicx+xMBhHpfWl2QksYaaAh4ksjSJGLAx+iUbra7U3K4ElzO1KaQw/I43YjPTaWnuZQO1ZWbt6T6P1jaO9OG1lu6STVp/NIwK1xm3izNukk+68b5grYI0FHWN0VtbbcKVrzIDNU1LK4GUGAnZxsF4rbDH3UGJZT1bWr97X6Dqfw9158a8QbzKD1jj4nJZNYDxBVrperK5fvW1FoNn72o/zTXFHq3RJMbILNy+aJwRqgXhcRm/c6+AgnuaYYrLy3sY3ijoYTCYwD44/+egugcoiSSuJe/W6zuf1z0eHB1fifG/s4WY2pf2YtBPxnZX3ruo7DwSxqIJ4HczduN8aW8BsaI+qgOFtiLh6VEa8FhNQbGMZ24UWKuFGj4A7XRncKYNNVFZBvEncZyHRfOBKuNEjxJ1e3/gQ1Vq6+eN6+4Yx1bC8jgTus5BIwJVxo3+mLW4XRKwTKZFksqcLVfEKKZn7LCRyoYXV9Y0vKrPbjcNm+/jTj3YIeI9suMMy2Q5ViKTus5DIAguVcqPHkeTW+gassedIgUY/MB9SxcTrMJzYgCQWcH+JHlB16did/Qtkqf1j5c/tjmywhvhBlVzmcRopCqYSu9CCvJBUxR1wDNeLN+Cdo0/CXQKl4Y6HAnPPmKo37uf948MwcVfVxBbYkcLUa0PifBOZRxLzo0VP8YhwV9/beCQeUfXFS2J+U3m2qSywkO+ZMHdlnEQQ/NwNxETUElHZX3jD/qlDBTOyyI0BdZ8/D3sEckFcZRq47hkdqglpklcjmpSWiB/azG2msaL8QxqGN+cJxLlV8gbbn19URtz9HGuR+/aXtJZhNzK8//lhWLVjtVKQm2LN1yO5PXTDird+Vz8X8GhTW+Dltn3RfzRfZGqFmXeOPw23kzxldc26twUKeRxnlZm6JuDHxx+HXQKxcUdBDbphN0WZadWhmiJr6OwlX4tz93fK89OzstbeztgKd48PDzYpIbKZNH6gbXuof5vKQqZYGOpywHKPM5w2kLmuOCv7G2rTILKC5Y79EposkNgs3j36NLxFKVlIwM4K/2C+oSxZoKBCrLEJzH0fjhlkZ3UTHgN+Jt0FpT1o2l1WIy7MaVDHRNEGBDudpuEri+RUFhKwsLre3jWU8TSEBUTs+a0UscpWxCbkIDgwTD3NwharurRErTOmNgc2wWjMhs0Yt+2qWq7qWW2WLGp93d9BCzIsa/uCsiYwD/rfBTtpFrfSq2V7/Zd8S4OY5QIBSykjRLoQi1pfoUEL8tVXL07e/P1lSYFn6yIZ/ufGBfrr7y5f/vKb/3nxPMlT5Xd6+63L+9bCycG494tM3G37Zv7L8XH4ghTwzVcvnr/x5uWL9hfvEEiFWN/PDsOFy5IXtsBCblZ4CDPtNiLeSbpbud5Ixjz12lLY5Fcz4msaz5ZX3tuQm0CJq4dANtZXSFeJ9QruF8mxOksqcdJc+3OZ4IHnVWMm+cbkC/1/4FvuKA0kQqxvVu95JgIW3CWHHAeCSzybRsTHfwsfGOKH5CW8d3wYqr0cIj3DjeGFkjB1wxUrRZSZUclMwK4BfJSvUEYiTtrjefCanBH7ZymahtS39Dl+7gpYugRiYZgfZ+lxZSZgQaxw3kJJ0+PZR0uRpRtVOjW43JIFoo2klYbzyFTAIpSIuAir0ln509X7SZ7gLAX70xIoSzeqbGCFY5LDRpepgIVhgX+X8kaa0snNlQT4k3Th6t1oghWeA+8dPc/+bnnmAhaaBbmrPDCPksTDvrjSbJI1LtPA0MOpTaloUvLKd+Qi4LyPlUa4ePhSlOgyhVtoEZeaPGpU1N2sbL+0ReH8jgpzEbBQRELLkcKVlqOl8ly+CrrPP9Ml8AvySFyNk5uAC3VXB8mvNFpLvF2GiKXTCFUU5cPg8yHgDyhHchOwIO5qQUUUnTR9q0oScZcqimzaqMz6GVn7ed8Lz1XAQlFFFCYy99MM8R6KeLOohRcEVV/gqofBZ4asp8FL2qacyV3ABbrSrebrlGoiu3gKDSfivJMw3K16pw6540yAZD0VcTU0dwELhWV+jbmdxgoLklg6Ogy35JZIpkK2RytSdSVW/vgw3KSKwxBwrlnnX/0oKpBiGsLz3SwuCIw6X1qLcjNpG1u7AfQion0ZBtc/rVcrHXfZn5KVulYL7ha5UadvK5vmh1lX2s21yfV+rutyuLCAhzvorny4hmxLrlWMfLTsrvfuL34i0ZdDyxOefUe9Ogn2VYI+9Uyhq8ofztsiU6GFQoVaYGFlrS39nJ9SnjSsu4o2r6WQd3MHnzED/uDzz4rtFV5IDDyOi4fzProZZNtwHoC52Li3aPEKhQtYcEc3+d4M6iStzgIgNcx7eVZbzaIUAQu53wyCFQYFIGu4/32xce84pQlYzocb+RZQdFbW26nOhQGIw2iWV5lJy9IELLhMr9SK5nYNzdyHKw1yw67dsi+mlCpgQSqTOM8ij4F5krQRHgDzYHsk6kNVXekCFlyngvwy06003SwBmIrNOOfRXSMNXghYyPNmkFz8t2eTH6a5sQTAL0gxAjdPFh6tkiVff/Wi+8ZblzmnkR1LMkXgjXfeab395uW/yfgVApnz1lt/aEVM1UweeiZewRsLPEIsca53iCO+Iy41rDFIglxw8U28QuGllHFZXb+6a4Wc7djSV3BHWAHvNAZU5TY3hVLFUkoRr9xUIw/xVsBCESIeY48D3s9LzKMkWtU3iqoJ2GfxCl4LWFhZa2/bnH2hVVVimY0M42Y+MIZCbtBJs0+901M6mXRoPxp0PWhQK4poOQioZZ+3zGTeHR94bTcIOXrYpQpTJQH7Ll7B+4tfEhNbEVORIh4OBreiMzfcDjcg6tsHzYvuTvPEZ/TlU3S+I5rop7/nlQdADXJU5GHM+yreJbEmUVYHSVBTPMw2T0OFgAUn4pIbsoM6wHe1iFdQI2Bh2JB9EyM8QOZI7zLDt7TNa1YlYEEaAjQjvob+wyArZC0x86Yv5ZFJUCdgQY5iGueWuNItWkEhhLKWtLb7VSlgQUR8/OnBNSS3QFqk4q//kjc1n82rFfCIn5JbiItBIvju54fhHe0dRNULWJDkFuJiEAcX7wZ8TVuyahqVELAgbtDZa3ytoGFqqZDqLALlwbx39pKvVWm8TaVacEufLfvpzupaO5TKrWFFFSiQvo+vuYRXxhVnVMLqjlMZCzyOHAcUM6wM+A+7Y8equMyvUtkhGMPM4pa1xl1Y4xoysroVFe6ISlrgcWCN60i1re44tRhDBWtcE2pidcepvAUeR6zx0eHBFRR/VA9XlPE9X6mTeIVaCXiEFH9kPsgblATLsLzNKhRlpKGWAhbErZZuC9zna6ip1ocr2rHClWHabuJlTamtgEccHYWh1FTLVTJUcilgeO1PQqE6C3dE7QU8YhQfQ8ieMkxQSZyr8dpfXtQiC52E4eLYtRnrLcN006ZHOgRK47zBoCSoaLeOMe48IOApjIS8stbuMNOWIbNwe1uOqm/Z+0sUNn+gDJDkFMkMoi6BqXjfVtYXpF3qgGnbWuTrac+RJfNdhwbyK+sbT+2nDiXl3E0W4T5EfBsPCDgFqdxrRZ0OF0W8FmLzNP4zOLSi3e+f0gO4ycmAgBdArPIZ052AzPuzrLLEcHJOSTVi5Z/adygw96d+g7W2xvBjNrQHa5seCDgjxOpYqyyN4M87v7sFSl/WeYGONjj3mjC17E52YvMJPRPRY2ttQ1hbAAAAAAAAAAAAAAAAAAAAAAAA4Jf8P8WdqoAaqCHmAAAAAElFTkSuQmCC" +) + +type GithubOidcRouter struct { + config_obj *config_proto.Config } -type GitHubAuthenticator struct{} +func (self *GithubOidcRouter) Name() string { + return "GitHub" +} -func (self *GitHubAuthenticator) IsPasswordLess() bool { - return true +func (self *GithubOidcRouter) LoginHandler() string { + return "/auth/github/login" } -func (self *GitHubAuthenticator) AddHandlers(config_obj *config_proto.Config, mux *http.ServeMux) error { - mux.Handle("/auth/github/login", oauthGithubLogin(config_obj)) - mux.Handle("/auth/github/callback", oauthGithubCallback(config_obj)) +func (self *GithubOidcRouter) CallbackHandler() string { + return "/auth/github/callback" +} - installLogoff(config_obj, mux) - return nil +func (self *GithubOidcRouter) Scopes() []string { + return []string{"user:email"} } -// Check that the user is proerly authenticated. -func (self *GitHubAuthenticator) AuthenticateUserHandler( - config_obj *config_proto.Config, - parent http.Handler) http.Handler { +func (self *GithubOidcRouter) Issuer() string { + return "https://github.com/login/oauth" +} - return authenticateUserHandle( - config_obj, parent, "/auth/github/login", "GitHub") +func (self *GithubOidcRouter) Endpoint() oauth2.Endpoint { + return github.Endpoint } -func oauthGithubLogin(config_obj *config_proto.Config) http.Handler { - authenticator := config_obj.GUI.Authenticator - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var githubOauthConfig = &oauth2.Config{ - RedirectURL: config_obj.GUI.PublicUrl + "auth/github/callback", - ClientID: authenticator.OauthClientId, - ClientSecret: authenticator.OauthClientSecret, - Scopes: []string{"user:email"}, - Endpoint: github.Endpoint, - } - - // Create oauthState cookie - oauthState, err := r.Cookie("oauthstate") - if err != nil { - oauthState = generateStateOauthCookie(w) - } - - u := githubOauthConfig.AuthCodeURL(oauthState.Value, oauth2.ApprovalForce) - http.Redirect(w, r, u, http.StatusTemporaryRedirect) - }) +func (self *GithubOidcRouter) SetEndpoint(oauth2.Endpoint) {} + +func (self *GithubOidcRouter) Avatar() string { + return GithubIcon } -func oauthGithubCallback(config_obj *config_proto.Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Read oauthState from Cookie - oauthState, _ := r.Cookie("oauthstate") - - if r.FormValue("state") != oauthState.Value { - logging.GetLogger(config_obj, &logging.GUIComponent). - Error("invalid oauth github state") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - data, err := getUserDataFromGithub( - r.Context(), config_obj, r.FormValue("code")) - if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - WithFields(logrus.Fields{ - "err": err, - }).Error("getUserDataFromGithub") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - user_info := &GitHubUser{} - err = json.Unmarshal(data, &user_info) - if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - WithFields(logrus.Fields{ - "err": err, - }).Error("getUserDataFromGithub") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - // Create a new token object, specifying signing method and the claims - // you would like it to contain. - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "user": user_info.Login, - // Required re-auth after one day. - "expires": float64(time.Now().AddDate(0, 0, 1).Unix()), - "picture": user_info.AvatarUrl, - }) - - // Sign and get the complete encoded token as a string using the secret - tokenString, err := token.SignedString( - []byte(config_obj.Frontend.PrivateKey)) - if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - WithFields(logrus.Fields{ - "err": err, - }).Error("getUserDataFromGithub") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - // Set the cookie and redirect. - cookie := &http.Cookie{ - Name: "VelociraptorAuth", - Value: tokenString, - Path: "/", - Secure: true, - HttpOnly: true, - Expires: time.Now().AddDate(0, 0, 1), - } - http.SetCookie(w, cookie) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - }) +func (self *GithubOidcRouter) LoginURL() string { + return api_utils.PublicURL(self.config_obj, self.LoginHandler()) } -func getUserDataFromGithub( - ctx context.Context, - config_obj *config_proto.Config, code string) ([]byte, error) { - authenticator := config_obj.GUI.Authenticator - - // Use code to get token and get user info from GitHub. - var githubOauthConfig = &oauth2.Config{ - RedirectURL: config_obj.GUI.PublicUrl + "auth/github/callback", - ClientID: authenticator.OauthClientId, - ClientSecret: authenticator.OauthClientSecret, - Scopes: []string{}, - Endpoint: github.Endpoint, - } +// Github does not actually support oidc so we need a pure oauth2 +// claims getter. - token, err := githubOauthConfig.Exchange(ctx, code) - if err != nil { - return nil, fmt.Errorf("code exchange wrong: %s", err.Error()) +type GitHubUser struct { + Login string `json:"login"` + AvatarUrl string `json:"avatar_url"` +} + +type GithubClaimsGetter struct { + config_obj *config_proto.Config +} + +func (self *GithubClaimsGetter) GetClaims( + ctx *HTTPClientContext, token *oauth2.Token) (claims *Claims, err error) { + + githubOauthConfig := &oauth2.Config{ + Endpoint: github.Endpoint, } response, err := githubOauthConfig.Client(ctx, token).Get("https://api.github.com/user") if err != nil { - return nil, fmt.Errorf("failed getting user info: %s", err.Error()) + return nil, fmt.Errorf("failed getting user info: %v", err) } defer response.Body.Close() - contents, err := ioutil.ReadAll( - io.LimitReader(response.Body, constants.MAX_MEMORY)) + contents, err := utils.ReadAllWithLimit(response.Body, constants.MAX_MEMORY) + if err != nil { + return nil, fmt.Errorf("failed read response: %v", err) + } + + user_info := &GitHubUser{} + err = json.Unmarshal(contents, &user_info) if err != nil { - return nil, fmt.Errorf("failed read response: %s", err.Error()) + return nil, err } - return contents, nil + // Update the user picture in the datastore if we can - it + // will be populated from there for each GetUserUITraits + // call. This keeps our cookie smaller. + setUserPicture(ctx, user_info.Login, user_info.AvatarUrl) + + return &Claims{ + Username: user_info.Login, + }, nil } diff --git a/api/authenticators/google.go b/api/authenticators/google.go index bb58e8a90..393b79042 100644 --- a/api/authenticators/google.go +++ b/api/authenticators/google.go @@ -1,365 +1,63 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package authenticators import ( - "crypto/rand" - "encoding/base64" - "errors" - "fmt" - "io" - "io/ioutil" - "net/http" - "time" - - jwt "github.com/golang-jwt/jwt" - "github.com/gorilla/csrf" - "github.com/sirupsen/logrus" - context "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" - "www.velocidex.com/golang/velociraptor/acls" - api_proto "www.velocidex.com/golang/velociraptor/api/proto" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" - "www.velocidex.com/golang/velociraptor/constants" - "www.velocidex.com/golang/velociraptor/json" - "www.velocidex.com/golang/velociraptor/logging" - users "www.velocidex.com/golang/velociraptor/users" ) -const oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" - -type GoogleAuthenticator struct{} - -func (self *GoogleAuthenticator) AddHandlers(config_obj *config_proto.Config, mux *http.ServeMux) error { - mux.Handle("/auth/google/login", oauthGoogleLogin(config_obj)) - mux.Handle("/auth/google/callback", oauthGoogleCallback(config_obj)) - - installLogoff(config_obj, mux) - - return nil +type GoogleOidcRouter struct { + config_obj *config_proto.Config } -func (self *GoogleAuthenticator) IsPasswordLess() bool { - return true +func (self *GoogleOidcRouter) Name() string { + return "Google" } -// Check that the user is proerly authenticated. -func (self *GoogleAuthenticator) AuthenticateUserHandler( - config_obj *config_proto.Config, - parent http.Handler) http.Handler { - - return authenticateUserHandle( - config_obj, parent, "/auth/google/login", "Google") +func (self *GoogleOidcRouter) LoginHandler() string { + return "/auth/google/login" } -func oauthGoogleLogin(config_obj *config_proto.Config) http.Handler { - authenticator := config_obj.GUI.Authenticator - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var googleOauthConfig = &oauth2.Config{ - RedirectURL: config_obj.GUI.PublicUrl + "auth/google/callback", - ClientID: authenticator.OauthClientId, - ClientSecret: authenticator.OauthClientSecret, - Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"}, - Endpoint: google.Endpoint, - } - - // Create oauthState cookie - oauthState, err := r.Cookie("oauthstate") - if err != nil { - oauthState = generateStateOauthCookie(w) - } - - u := googleOauthConfig.AuthCodeURL(oauthState.Value, oauth2.ApprovalForce) - http.Redirect(w, r, u, http.StatusTemporaryRedirect) - }) +func (self *GoogleOidcRouter) CallbackHandler() string { + return "/auth/google/callback" } -func generateStateOauthCookie(w http.ResponseWriter) *http.Cookie { - // Do not expire from the browser - we will expire it anyway. - var expiration = time.Now().Add(365 * 24 * time.Hour) - - b := make([]byte, 16) - _, _ = rand.Read(b) - state := base64.URLEncoding.EncodeToString(b) - cookie := http.Cookie{ - Name: "oauthstate", - Value: state, - Secure: true, - HttpOnly: true, - Expires: expiration} - http.SetCookie(w, &cookie) - - return &cookie +func (self *GoogleOidcRouter) Scopes() []string { + return []string{"https://www.googleapis.com/auth/userinfo.email"} } -func oauthGoogleCallback(config_obj *config_proto.Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Read oauthState from Cookie - oauthState, _ := r.Cookie("oauthstate") - - if r.FormValue("state") != oauthState.Value { - logging.GetLogger(config_obj, &logging.GUIComponent). - Error("invalid oauth google state") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - data, err := getUserDataFromGoogle( - r.Context(), config_obj, r.FormValue("code")) - if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - WithFields(logrus.Fields{ - "err": err, - }).Error("getUserDataFromGoogle") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - user_info := &api_proto.VelociraptorUser{} - err = json.Unmarshal(data, &user_info) - if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - WithFields(logrus.Fields{ - "err": err, - }).Error("getUserDataFromGoogle") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - // Create a new token object, specifying signing method and the claims - // you would like it to contain. - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "user": user_info.Email, - // Required re-auth after one day. - "expires": float64(time.Now().AddDate(0, 0, 1).Unix()), - "picture": user_info.Picture, - }) - - // Sign and get the complete encoded token as a string using the secret - tokenString, err := token.SignedString( - []byte(config_obj.Frontend.PrivateKey)) - if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - WithFields(logrus.Fields{ - "err": err, - }).Error("getUserDataFromGoogle") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - // Set the cookie and redirect. - cookie := &http.Cookie{ - Name: "VelociraptorAuth", - Value: tokenString, - Path: "/", - Secure: true, - HttpOnly: true, - Expires: time.Now().AddDate(0, 0, 1), - } - http.SetCookie(w, cookie) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - }) +func (self *GoogleOidcRouter) Issuer() string { + return "https://accounts.google.com" } -func getUserDataFromGoogle( - ctx context.Context, - config_obj *config_proto.Config, - code string) ([]byte, error) { - authenticator := config_obj.GUI.Authenticator - // Use code to get token and get user info from Google. - var googleOauthConfig = &oauth2.Config{ - RedirectURL: config_obj.GUI.PublicUrl + "auth/google/callback", - ClientID: authenticator.OauthClientId, - ClientSecret: authenticator.OauthClientSecret, - Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"}, - Endpoint: google.Endpoint, - } - - token, err := googleOauthConfig.Exchange(ctx, code) - if err != nil { - return nil, fmt.Errorf("code exchange wrong: %s", err.Error()) - } - response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken) - if err != nil { - return nil, fmt.Errorf("failed getting user info: %s", err.Error()) - } - defer response.Body.Close() - - contents, err := ioutil.ReadAll( - io.LimitReader(response.Body, constants.MAX_MEMORY)) - if err != nil { - return nil, fmt.Errorf("failed read response: %s", err.Error()) - } - return contents, nil +func (self *GoogleOidcRouter) Endpoint() oauth2.Endpoint { + return google.Endpoint } -func installLogoff(config_obj *config_proto.Config, mux *http.ServeMux) { - // On logoff just clear the cookie and redirect. - mux.Handle("/logoff", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - params := r.URL.Query() - old_username, ok := params["username"] - if ok && len(old_username) == 1 { - logger := logging.GetLogger(config_obj, &logging.Audit) - logger.Info("Logging off %v", old_username[0]) - } - http.SetCookie(w, &http.Cookie{ - Name: "VelociraptorAuth", - Path: "/", - Value: "", - Secure: true, - HttpOnly: true, - Expires: time.Unix(0, 0), - }) - fmt.Fprintf(w, ` - - You have successfully logged off! - - `) - })) -} +func (self *GoogleOidcRouter) SetEndpoint(oauth2.Endpoint) {} -func authenticateUserHandle(config_obj *config_proto.Config, - parent http.Handler, login_url string, provider string) http.Handler { - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-CSRF-Token", csrf.Token(r)) - - // Reject by redirecting to the login handler. - reject_with_username := func(err error, username string) { - logger := logging.GetLogger(config_obj, &logging.Audit) - logger.WithFields(logrus.Fields{ - "remote": r.RemoteAddr, - "error": err.Error(), - }).Error("OAuth2 Redirect") - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(http.StatusUnauthorized) - - fmt.Fprintf(w, ` - -Authorization failed. You are not registered on this system as %v. -Contact your system administrator to get an account, or click here -to log in again: - - - Login with %s - - -`, username, login_url, provider) - - logging.GetLogger(config_obj, &logging.Audit). - WithFields(logrus.Fields{ - "user": username, - "remote": r.RemoteAddr, - "method": r.Method, - }).Error("User rejected by GUI") - } - - reject := func(err error) { - reject_with_username(err, "") - } - - // We store the user name and their details in a local - // cookie. It is stored as a JWT so we can trust it. - auth_cookie, err := r.Cookie("VelociraptorAuth") - if err != nil { - reject(err) - return - } - - // Parse the JWT. - token, err := jwt.Parse( - auth_cookie.Value, - func(token *jwt.Token) (interface{}, error) { - _, ok := token.Method.(*jwt.SigningMethodHMAC) - if !ok { - return nil, errors.New("invalid signing method") - } - return []byte(config_obj.Frontend.PrivateKey), nil - }) - if err != nil { - reject(err) - return - } - - claims, ok := token.Claims.(jwt.MapClaims) - if !ok || !token.Valid { - reject(errors.New("token not valid")) - return - } - - // Record the username for handlers lower in the - // stack. - username, pres := claims["user"].(string) - if !pres { - reject(errors.New("username not present")) - return - } - - // Check if the claim is too old. - expires, pres := claims["expires"].(float64) - if !pres { - reject_with_username(errors.New("expires field not present in JWT"), - username) - return - } - - if expires < float64(time.Now().Unix()) { - reject_with_username(errors.New("the JWT is expired - reauthenticate"), - username) - return - } - - picture, _ := claims["picture"].(string) - - // Now check if the user is allowed to log in. - user_record, err := users.GetUser(config_obj, username) - if err != nil { - reject_with_username(errors.New("Invalid user"), username) - return - } - - // Must have at least reader permission. - perm, err := acls.CheckAccess(config_obj, username, acls.READ_RESULTS) - if !perm || err != nil || user_record.Locked || user_record.Name != username { - reject_with_username(errors.New("Insufficient permissions"), username) - return - } - - // Checking is successful - user authorized. Here we - // build a token to pass to the underlying GRPC - // service with metadata about the user. - user_info := &api_proto.VelociraptorUser{ - Name: username, - Picture: picture, - } - - // Must use json encoding because grpc can not handle - // binary data in metadata. - serialized, _ := json.Marshal(user_info) - ctx := context.WithValue( - r.Context(), constants.GRPC_USER_CONTEXT, string(serialized)) +func (self *GoogleOidcRouter) Avatar() string { + return "" +} - // Need to call logging after auth so it can access - // the contextKeyUser value in the context. - GetLoggingHandler(config_obj)(parent).ServeHTTP( - w, r.WithContext(ctx)) - }) +func (self *GoogleOidcRouter) LoginURL() string { + return api_utils.PublicURL(self.config_obj, self.LoginHandler()) } diff --git a/api/authenticators/http.go b/api/authenticators/http.go new file mode 100644 index 000000000..242ab83fb --- /dev/null +++ b/api/authenticators/http.go @@ -0,0 +1,107 @@ +package authenticators + +import ( + "bytes" + "context" + "io" + "net/http" + + oidc "github.com/coreos/go-oidc/v3/oidc" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/vql/networking" +) + +type transformerTransport struct { + transport http.RoundTripper + transformers []Transformer +} + +func (self *transformerTransport) RoundTrip( + req *http.Request) (*http.Response, error) { + + rt := self.transport.RoundTrip + if rt == nil { + rt = http.DefaultTransport.RoundTrip + } + + for _, t := range self.transformers { + rt = t(rt) + } + + return rt(req) +} + +// The OIDC libraries force us to embed the http client inside the +// context but this is error prone because we can accidentally feed +// them a regular context (without a custom http client). This +// transparently uses the default http client which breaks in cases we +// need proxies etc. +// To fix this we require a new type for a HTTP adorned context. +type HTTPClientContext struct { + context.Context + HTTPClient *http.Client +} + +// Update the HTTP client in the context honoring proxy and TLS +// settings in the config file. This is needed to pass to oidc +// functions that will make HTTP calls. +func ClientContext( + ctx context.Context, + config_obj *config_proto.Config, + transformers []Transformer) (*HTTPClientContext, error) { + transport, err := networking.GetHttpTransport(config_obj.Client, "") + if err != nil { + return nil, err + } + + // Allow the context to be spied on if needed. + transport = networking.MaybeSpyOnTransport(config_obj, transport) + + client := &http.Client{ + Transport: &transformerTransport{ + transport: transport, + transformers: transformers, + }, + } + + return &HTTPClientContext{ + Context: oidc.ClientContext(ctx, client), + HTTPClient: client, + }, nil +} + +func DefaultTransforms( + config_obj *config_proto.Config, + authenticator *config_proto.Authenticator) []Transformer { + if authenticator.OidcDebug { + return []Transformer{traceNetwork(config_obj)} + } + return nil +} + +func traceNetwork(config_obj *config_proto.Config) func(rt RoundTripFunc) RoundTripFunc { + return func(rt RoundTripFunc) RoundTripFunc { + return func(req *http.Request) (*http.Response, error) { + res, err := rt(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + ct, _ := res.Header["Content-Type"] + + bs, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + logger := logging.GetLogger(config_obj, &logging.GUIComponent) + logger.Debug("oidc: Calling URL: %v, Response: %v (CT %v)", + req.URL, string(bs), ct) + + res.Body = io.NopCloser(bytes.NewReader(bs)) + return res, nil + } + } +} diff --git a/api/authenticators/ip_filter.go b/api/authenticators/ip_filter.go new file mode 100644 index 000000000..b7dd56f64 --- /dev/null +++ b/api/authenticators/ip_filter.go @@ -0,0 +1,75 @@ +package authenticators + +import ( + "net" + "net/http" + "strings" + + api_utils "www.velocidex.com/golang/velociraptor/api/utils" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" +) + +// Implement a src IP filter if required. This adds an additional +// layer of protection on the GUI. +func IpFilter(config_obj *config_proto.Config, + parent http.Handler) http.Handler { + + if config_obj.GUI == nil || len(config_obj.GUI.AllowedCidr) == 0 { + return api_utils.HandlerFunc(parent, parent.ServeHTTP) + } + + ranges := []*net.IPNet{} + for _, cidr := range config_obj.GUI.AllowedCidr { + _, cidr_net, err := net.ParseCIDR(cidr) + if err != nil { + // Should never happen because sanity service should check + // it already. + panic("Invalid CIDR Range " + cidr) + } + ranges = append(ranges, cidr_net) + } + + return api_utils.HandlerFunc(parent, + func(w http.ResponseWriter, r *http.Request) { + + // If the user specified a forwarded header and the header is + // there we must check it. + if config_obj.GUI.ForwardedProxyHeader != "" { + address_string := r.Header.Get(config_obj.GUI.ForwardedProxyHeader) + ips := strings.Split(address_string, ", ") + if len(ips) > 0 { + // CIDR matched allow it. + if matchCidr(ranges, ips...) { + parent.ServeHTTP(w, r) + return + } + http.Error(w, "rejected", http.StatusUnauthorized) + return + } + } + + // Try to check the remote address now. + remote_address := strings.Split(r.RemoteAddr, ":")[0] + if matchCidr(ranges, remote_address) { + parent.ServeHTTP(w, r) + return + } + http.Error(w, "rejected", http.StatusUnauthorized) + }) +} + +func matchCidr(ranges []*net.IPNet, ip_strings ...string) bool { + for _, ip_str := range ip_strings { + ip := net.ParseIP(ip_str) + if ip == nil { + return false + } + + for _, cidr := range ranges { + if cidr.Contains(ip) { + return true + } + } + } + return false +} diff --git a/api/authenticators/logging.go b/api/authenticators/logging.go index c6ccac206..efa9d17c7 100644 --- a/api/authenticators/logging.go +++ b/api/authenticators/logging.go @@ -6,33 +6,14 @@ import ( "github.com/sirupsen/logrus" api_proto "www.velocidex.com/golang/velociraptor/api/proto" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/logging" + http_utils "www.velocidex.com/golang/velociraptor/utils/http" ) -// Record the status of the request so we can log it. -type statusRecorder struct { - http.ResponseWriter - http.Flusher - status int - error []byte -} - -func (self *statusRecorder) WriteHeader(code int) { - self.status = code - self.ResponseWriter.WriteHeader(code) -} - -func (self *statusRecorder) Write(buf []byte) (int, error) { - if self.status == 500 { - self.error = buf - } - - return self.ResponseWriter.Write(buf) -} - func GetUserInfo(ctx context.Context, config_obj *config_proto.Config) *api_proto.VelociraptorUser { result := &api_proto.VelociraptorUser{} @@ -51,40 +32,42 @@ func GetUserInfo(ctx context.Context, func GetLoggingHandler(config_obj *config_proto.Config) func(http.Handler) http.Handler { logger := logging.GetLogger(config_obj, &logging.GUIComponent) + return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rec := &statusRecorder{ - w, - w.(http.Flusher), - 200, nil} - defer func() { - if rec.status == 500 { - logger.WithFields( - logrus.Fields{ - "method": r.Method, - "url": r.URL.Path, - "remote": r.RemoteAddr, - "error": string(rec.error), - "user-agent": r.UserAgent(), - "status": rec.status, - "user": GetUserInfo( - r.Context(), config_obj).Name, - }).Error("") + return api_utils.HandlerFunc(next, + func(w http.ResponseWriter, r *http.Request) { + rec := &http_utils.StatusRecorder{ + ResponseWriter: w, + Flusher: w.(http.Flusher), + Status: 200} + defer func() { + if rec.Status == 500 { + logger.WithFields( + logrus.Fields{ + "method": r.Method, + "url": r.URL.Path, + "remote": r.RemoteAddr, + "error": string(rec.Error), + "user-agent": r.UserAgent(), + "status": rec.Status, + "user": GetUserInfo( + r.Context(), config_obj).Name, + }).Error("") - } else { - logger.WithFields( - logrus.Fields{ - "method": r.Method, - "url": r.URL.Path, - "remote": r.RemoteAddr, - "user-agent": r.UserAgent(), - "status": rec.status, - "user": GetUserInfo( - r.Context(), config_obj).Name, - }).Info("") - } - }() - next.ServeHTTP(rec, r) - }) + } else { + logger.WithFields( + logrus.Fields{ + "method": r.Method, + "url": r.URL.Path, + "remote": r.RemoteAddr, + "user-agent": r.UserAgent(), + "status": rec.Status, + "user": GetUserInfo( + r.Context(), config_obj).Name, + }).Info("") + } + }() + next.ServeHTTP(rec, r) + }) } } diff --git a/api/authenticators/logoff.go b/api/authenticators/logoff.go new file mode 100644 index 000000000..711ceb53d --- /dev/null +++ b/api/authenticators/logoff.go @@ -0,0 +1,45 @@ +package authenticators + +import ( + "net/http" + "time" + + "github.com/Velocidex/ordereddict" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/services" +) + +func installLogoff(config_obj *config_proto.Config, mux *api_utils.ServeMux) { + mux.Handle(api_utils.GetBasePath(config_obj, "/app/logoff.html"), + IpFilter(config_obj, + api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + old_username, ok := params["username"] + username := "" + if ok && len(old_username) == 1 { + err := services.LogAudit(r.Context(), + config_obj, old_username[0], "LogOff", ordereddict.NewDict()) + if err != nil { + logger := logging.GetLogger( + config_obj, &logging.FrontendComponent) + logger.Error("LogAudit: LogOff %v", old_username[0]) + } + username = old_username[0] + } + + // Clear the cookie + http.SetCookie(w, &http.Cookie{ + Name: "VelociraptorAuth", + Path: api_utils.GetBaseDirectory(config_obj), + Value: "deleted", + Secure: true, + HttpOnly: true, + Expires: time.Unix(0, 0), + }) + + renderLogoffMessage(config_obj, w, username) + }))) +} diff --git a/api/authenticators/multiple.go b/api/authenticators/multiple.go new file mode 100644 index 000000000..4a7d32c02 --- /dev/null +++ b/api/authenticators/multiple.go @@ -0,0 +1,125 @@ +package authenticators + +import ( + "fmt" + "net/http" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/acls" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/gui/velociraptor" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/services" +) + +type MultiAuthenticator struct { + delegates []Authenticator + config_obj *config_proto.Config + delegate_info []velociraptor.AuthenticatorInfo +} + +func (self *MultiAuthenticator) Delegates() []Authenticator { + return self.delegates +} + +func (self *MultiAuthenticator) AddHandlers(mux *api_utils.ServeMux) error { + for _, delegate := range self.delegates { + err := delegate.AddHandlers(mux) + if err != nil { + return err + } + } + return nil +} + +func (self *MultiAuthenticator) AddLogoff(mux *api_utils.ServeMux) error { + installLogoff(self.config_obj, mux) + return nil +} + +func (self *MultiAuthenticator) reject_with_username( + w http.ResponseWriter, r *http.Request, err error, username string) { + + // Log into the audit log. + if username != "" { + err := services.LogAudit(r.Context(), + self.config_obj, username, "User rejected by GUI", + ordereddict.NewDict(). + Set("remote", r.RemoteAddr). + Set("method", r.Method). + Set("err", err.Error())) + if err != nil { + logger := logging.GetLogger(self.config_obj, &logging.FrontendComponent) + logger.Error("MultiAuthenticator reject_with_username %v %v", + username, r.RemoteAddr) + } + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusUnauthorized) + + renderRejectionMessage( + self.config_obj, r, w, err, + username, self.delegate_info) +} + +func (self *MultiAuthenticator) AuthenticateUserHandler( + parent http.Handler, + permission acls.ACL_PERMISSION, +) http.Handler { + + return authenticateUserHandle( + self.config_obj, permission, + func(w http.ResponseWriter, r *http.Request, err error, username string) { + self.reject_with_username(w, r, err, username) + }, + parent) +} + +func (self *MultiAuthenticator) IsPasswordLess() bool { + return true +} + +func (self *MultiAuthenticator) RequireClientCerts() bool { + return false +} + +func (self *MultiAuthenticator) AuthRedirectTemplate() string { + return "" +} + +func NewMultiAuthenticator( + ctx *HTTPClientContext, + config_obj *config_proto.Config, + auth_config *config_proto.Authenticator) (Authenticator, error) { + result := &MultiAuthenticator{ + config_obj: config_obj, + } + for _, authenticator_config := range auth_config.SubAuthenticators { + auth, err := getAuthenticatorByType( + ctx, config_obj, authenticator_config) + if err != nil { + return nil, err + } + + // Only accept supported sub types. + switch t := auth.(type) { + case *OidcAuthenticator: + result.delegate_info = append(result.delegate_info, + velociraptor.AuthenticatorInfo{ + LoginURL: t.router.LoginURL(), + ProviderName: t.router.Name(), + ProviderAvatar: t.router.Avatar(), + }) + + default: + return nil, fmt.Errorf("MultiAuthenticator does not support %v as a child authenticator", + authenticator_config.Type) + } + + result.delegates = append(result.delegates, auth) + } + + return result, nil +} diff --git a/api/authenticators/oidc.go b/api/authenticators/oidc.go index 9718cb7b3..b4b688e0f 100644 --- a/api/authenticators/oidc.go +++ b/api/authenticators/oidc.go @@ -1,143 +1,229 @@ package authenticators import ( - "context" + "crypto/rand" + "encoding/base64" "net/http" - "strings" "time" - oidc "github.com/coreos/go-oidc" - jwt "github.com/golang-jwt/jwt" - "github.com/sirupsen/logrus" + "github.com/Velocidex/ordereddict" "golang.org/x/oauth2" + "www.velocidex.com/golang/velociraptor/acls" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/services" + utils "www.velocidex.com/golang/velociraptor/utils" ) -const ( - oidcLoginURI = "/auth/oidc/login" - oidcCallbackURI = "/auth/oidc/callback" -) +type OidcAuthenticator struct { + router OidcRouter + claims_getter ClaimsGetter + config_obj *config_proto.Config + authenticator *config_proto.Authenticator +} -type OidcAuthenticator struct{} +func (self *OidcAuthenticator) Name() string { + return self.router.Name() +} func (self *OidcAuthenticator) IsPasswordLess() bool { return true } -func (*OidcAuthenticator) AddHandlers(config_obj *config_proto.Config, mux *http.ServeMux) error { - provider, err := oidc.NewProvider(context.Background(), - config_obj.GUI.Authenticator.OidcIssuer) +func (self *OidcAuthenticator) RequireClientCerts() bool { + return false +} + +func (self *OidcAuthenticator) AuthRedirectTemplate() string { + return self.authenticator.AuthRedirectTemplate +} + +func (self *OidcAuthenticator) Provider() (ProviderInterface, error) { + oauth_config, err := self.GetGenOauthConfig() if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - Errorf("can not get information from OIDC provider, "+ - "check %v/.well-known/openid-configuration is correct and accessible from the server.", - config_obj.GUI.Authenticator.OidcIssuer) + return nil, err + } + + return NewProvider( + self.config_obj, oauth_config, self.authenticator, self.router, + self.claims_getter) +} + +func (self *OidcAuthenticator) AddHandlers(mux *api_utils.ServeMux) error { + provider, err := self.Provider() + if err != nil { + self.Error("can not get information from OIDC provider, "+ + "check %v/.well-known/openid-configuration is correct and accessible from the server.", + self.authenticator.OidcIssuer) return err } - mux.Handle(oidcLoginURI, oauthOidcLogin(config_obj, provider)) - mux.Handle(oidcCallbackURI, oauthOidcCallback(config_obj, provider)) + mux.Handle(api_utils.GetBasePath( + self.config_obj, self.router.LoginHandler()), + IpFilter(self.config_obj, self.oauthOidcLogin(provider))) + mux.Handle(api_utils.GetBasePath( + self.config_obj, self.router.CallbackHandler()), + IpFilter(self.config_obj, self.oauthOidcCallback(provider))) + return nil +} - installLogoff(config_obj, mux) +func (self *OidcAuthenticator) AddLogoff(mux *api_utils.ServeMux) error { + installLogoff(self.config_obj, mux) return nil } -func (*OidcAuthenticator) AuthenticateUserHandler( - config_obj *config_proto.Config, - parent http.Handler) http.Handler { +func (self *OidcAuthenticator) AuthenticateUserHandler( + parent http.Handler, + permission acls.ACL_PERMISSION, +) http.Handler { return authenticateUserHandle( - config_obj, parent, oidcLoginURI, "OIDC") + self.config_obj, permission, + func(w http.ResponseWriter, r *http.Request, err error, username string) { + reject_with_username(self.config_obj, w, r, err, username, + self.router.LoginHandler(), self.router.Name(), self.router.Avatar()) + }, + parent) } -func getGenOauthConfig( - config_obj *config_proto.Config, - endpoint oauth2.Endpoint, - callback string) *oauth2.Config { +func (self *OidcAuthenticator) GetGenOauthConfig() (*oauth2.Config, error) { - var scope []string - switch strings.ToLower(config_obj.GUI.Authenticator.Type) { - case "oidc": - scope = []string{oidc.ScopeOpenID, "email"} + callback := self.router.CallbackHandler() + res := &oauth2.Config{ + RedirectURL: api_utils.GetPublicURL(self.config_obj, callback), + ClientID: self.authenticator.OauthClientId, + ClientSecret: self.authenticator.OauthClientSecret, + Scopes: self.router.Scopes(), } - return &oauth2.Config{ - RedirectURL: config_obj.GUI.PublicUrl + callback[1:], - ClientID: config_obj.GUI.Authenticator.OauthClientId, - ClientSecret: config_obj.GUI.Authenticator.OauthClientSecret, - Scopes: scope, - Endpoint: endpoint, + self.Debug("OidcAuthenticator: OIDC configuration: %#v", res) + return res, nil +} + +// Ensure an XSRF protection for the auth flow by ensuring the +// callback is matched with the redirect by setting a cookie on the +// browser. +func generateStateOauthCookie( + config_obj *config_proto.Config, + w http.ResponseWriter) *http.Cookie { + var expiration = utils.GetTime().Now().Add(time.Hour) + + b := make([]byte, 16) + _, _ = rand.Read(b) + state := base64.URLEncoding.EncodeToString(b) + cookie := http.Cookie{ + Name: "oauthstate", + Path: api_utils.GetBasePath(config_obj), + Value: state, + Secure: true, + HttpOnly: true, + Expires: expiration} + http.SetCookie(w, &cookie) + return &cookie +} + +func (self *OidcAuthenticator) oauthOidcLogin(provider ProviderInterface) http.Handler { + + return api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + // Create oauthState cookie + oauthState, err := r.Cookie("oauthstate") + if err != nil { + oauthState = generateStateOauthCookie(self.config_obj, w) + } + + // Needed for Okta to specify `prompt: login` to avoid consent + // auth on each login. + var options []oauth2.AuthCodeOption + for k, v := range self.authenticator.OidcAuthUrlParams { + options = append(options, oauth2.SetAuthURLParam(k, v)) + } + + url := provider.GetRedirectURL(options, oauthState.Value) + + self.Debug("OidcAuthenticator: Redirecting to: %#v", url) + + http.Redirect(w, r, url, http.StatusFound) + }) +} + +func (self *OidcAuthenticator) Debug(message string, args ...interface{}) { + if self.authenticator.OidcDebug { + logging.GetLogger(self.config_obj, &logging.GUIComponent). + Debug(message, args...) } } -func oauthOidcLogin(config_obj *config_proto.Config, provider *oidc.Provider) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - oidcOauthConfig := getGenOauthConfig(config_obj, provider.Endpoint(), oidcCallbackURI) - - // Create oauthState cookie - oauthState, err := r.Cookie("oauthstate") - if err != nil { - oauthState = generateStateOauthCookie(w) - } - - url := oidcOauthConfig.AuthCodeURL(oauthState.Value, - oauth2.SetAuthURLParam("prompt", "login")) - http.Redirect(w, r, url, http.StatusFound) - }) -} - -func oauthOidcCallback(config_obj *config_proto.Config, provider *oidc.Provider) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Read oauthState from Cookie - oauthState, _ := r.Cookie("oauthstate") - if oauthState == nil || r.FormValue("state") != oauthState.Value { - logging.GetLogger(config_obj, &logging.GUIComponent). - Error("invalid oauth state of OIDC") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - oidcOauthConfig := getGenOauthConfig(config_obj, provider.Endpoint(), oidcCallbackURI) - oauthToken, err := oidcOauthConfig.Exchange(r.Context(), r.FormValue("code")) - if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - Error("can not get oauthToken from OIDC provider: %v", err) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - userInfo, err := provider.UserInfo(r.Context(), oauth2.StaticTokenSource(oauthToken)) - if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - Error("can not get UserInfo from OIDC provider") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "user": userInfo.Email, - "expires": float64(time.Now().AddDate(0, 0, 1).Unix()), +func (self *OidcAuthenticator) Error(message string, args ...interface{}) { + logging.GetLogger(self.config_obj, &logging.GUIComponent). + Error(message, args...) +} + +func (self *OidcAuthenticator) oauthOidcCallback( + provider ProviderInterface) http.Handler { + return api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + self.Debug("OidcAuthenticator: Received OIDC Callback %#v", r) + + // Read oauthState from Cookie and make sure the state + // that is passed back from the server are actually the + // same. + oauthState, _ := r.Cookie("oauthstate") + if oauthState == nil || r.FormValue("state") != oauthState.Value { + self.Error("invalid oauth state of OIDC: %v %v", + oauthState, r.FormValue("state")) + http.Redirect(w, r, api_utils.Homepage(self.config_obj), + http.StatusTemporaryRedirect) + return + } + + ctx, err := ClientContext(r.Context(), self.config_obj, + DefaultTransforms(self.config_obj, self.authenticator)) + if err != nil { + self.Error("OidcAuthenticator: %v", err) + http.Redirect(w, r, api_utils.Homepage(self.config_obj), + http.StatusTemporaryRedirect) + return + } + + code := r.FormValue("code") + cookie, claims, err := provider.GetJWT(ctx, code) + if err != nil { + self.Error("OidcAuthenticator: %v", err) + http.Redirect(w, r, api_utils.Homepage(self.config_obj), + http.StatusTemporaryRedirect) + return + } + + // Log a successful login. + err = services.LogAudit(r.Context(), + self.config_obj, claims.Username, "Login", + ordereddict.NewDict(). + Set("remote", r.RemoteAddr). + Set("authenticator", self.authenticator.Type). + Set("url", r.URL.Path)) + if err != nil { + self.Error("getSignedJWTTokenCookie LogAudit: Login %v %v", + claims.Username, r.RemoteAddr) + } + + self.Debug("oauthOidcCallback: Success! Setting cookie %#v", cookie) + + http.SetCookie(w, cookie) + http.Redirect(w, r, api_utils.Homepage(self.config_obj), + http.StatusTemporaryRedirect) }) +} - tokenString, err := token.SignedString( - []byte(config_obj.Frontend.PrivateKey)) - if err != nil { - logging.GetLogger(config_obj, &logging.GUIComponent). - WithFields(logrus.Fields{ - "err": err, - }).Error("can not get a signed tokenString") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - cookie := &http.Cookie{ - Name: "VelociraptorAuth", - Value: tokenString, - Path: "/", - HttpOnly: true, - Secure: true, - Expires: time.Now().AddDate(0, 0, 1), - } - http.SetCookie(w, cookie) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - }) +func NewOidcAuthenticator( + config_obj *config_proto.Config, + authenticator *config_proto.Authenticator, + router OidcRouter, + claims_getter ClaimsGetter) *OidcAuthenticator { + return &OidcAuthenticator{ + router: router, + claims_getter: claims_getter, + config_obj: config_obj, + authenticator: authenticator, + } } diff --git a/api/authenticators/orgs.go b/api/authenticators/orgs.go new file mode 100644 index 000000000..a8f2204da --- /dev/null +++ b/api/authenticators/orgs.go @@ -0,0 +1,136 @@ +package authenticators + +import ( + "errors" + "fmt" + "net/http" + "net/url" + + "www.velocidex.com/golang/velociraptor/acls" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" +) + +func GetOrgIdFromRequest(r *http.Request) string { + // Now we have to determine which org the user wants to use. First + // let's check if the user specified an org in the header. + org_id := r.Header.Get("Grpc-Metadata-Orgid") + if org_id != "" { + return org_id + } + + // Maybe the org id is specified in the URL itself. We allow + // the org id to be specified as a query string in order to + // support plain href links. However ultimately the GRPC + // gateway needs to check the org id in a header - so if an + // org is specified using a query string and NOT specified + // using a header, we set the header from it for further + // checks by the GRPC layer (in services/users/grpc.go) + q, err := url.ParseQuery(r.URL.RawQuery) + if err == nil { + org_id = q.Get("org_id") + if org_id != "" { + r.Header.Set("Grpc-Metadata-Orgid", org_id) + return org_id + } + } + + org_id = "root" + r.Header.Set("Grpc-Metadata-Orgid", org_id) + return org_id +} + +// Checks to make sure the user has access to the org they +// requested. If they do not have access to the org they requested we +// switch them to any org in which they have at least read +// access. This behaviour ensures that when a user's access is removed +// from an org the GUI immediately switches to the next available org. +func CheckOrgAccess( + config_obj *config_proto.Config, + r *http.Request, + user_record *api_proto.VelociraptorUser, + permission acls.ACL_PERMISSION) (err error) { + + org_id := GetOrgIdFromRequest(r) + err = _checkOrgAccess(r, org_id, permission, user_record) + if err == nil { + return nil + } + + // For the root org or an unknown org we switch to another org, + // otherwise we need to give the user a more specific error that + // they are not authorized for this org. + if !utils.IsRootOrg(org_id) && + !errors.Is(err, services.OrgNotFoundError) && + !errors.Is(err, utils.NoAccessToOrgError) { + return err + } + + ctx := r.Context() + + // Does the user already have a preferred org they want to be in? + user_manager := services.GetUserManager() + user_options, err := user_manager.GetUserOptions(ctx, user_record.Name) + if err != nil { + // Not an error - maybe the user never logged in yet + user_options = &api_proto.SetGUIOptionsRequest{} + } + + // Ok they are allowed to go to their preferred org. + err = _checkOrgAccess(r, user_options.Org, permission, user_record) + if err == nil { + r.Header.Set("Grpc-Metadata-Orgid", user_options.Org) + + // Log them into their org + return user_manager.SetUserOptions(ctx, user_record.Name, + user_record.Name, user_options) + } + + // Redirect the user to the first org they have access to + for _, org := range user_record.Orgs { + err = _checkOrgAccess(r, org.Id, permission, user_record) + if err == nil { + r.Header.Set("Grpc-Metadata-Orgid", org.Id) + + // Log them into their org + user_options.Org = org.Id + return user_manager.SetUserOptions(ctx, user_record.Name, + user_record.Name, user_options) + } + } + + if err != nil { + return fmt.Errorf("Unable to access any orgs: %w", err) + } + + return errors.New("Unauthorized username") +} + +func _checkOrgAccess(r *http.Request, + org_id string, permission acls.ACL_PERMISSION, + user_record *api_proto.VelociraptorUser) error { + org_manager, err := services.GetOrgManager() + if err != nil { + return err + } + + org_config_obj, err := org_manager.GetOrgConfig(org_id) + if err != nil { + return err + } + + perm, err := services.CheckAccess( + org_config_obj, user_record.Name, permission) + if err != nil { + return err + } + + if !perm || user_record.Locked { + return fmt.Errorf("User %v accessing %v: %w", + user_record.Name, org_id, utils.NoAccessToOrgError) + } + + return nil +} diff --git a/api/authenticators/provider.go b/api/authenticators/provider.go new file mode 100644 index 000000000..b1c7520e4 --- /dev/null +++ b/api/authenticators/provider.go @@ -0,0 +1,153 @@ +package authenticators + +import ( + "context" + "fmt" + "net/http" + "time" + + jwt "github.com/golang-jwt/jwt/v4" + "golang.org/x/oauth2" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/logging" + utils "www.velocidex.com/golang/velociraptor/utils" +) + +// Abstract oauth and oidc authenticators so we can reuse them in +// different types. + +// Transformers allow for stacking of round trippers. +type RoundTripFunc func(req *http.Request) (*http.Response, error) + +// A transformer can intercept network communications and change +// them. This is used to add debugging (to see what the oauth server +// is sending) and to mock out the network comms for testing. +type Transformer func(rt RoundTripFunc) RoundTripFunc + +// The provider's external methods. For oauth2 we only need two steps +// from the provider: The first is to form the redirect URL to the +// IDP, the second is to process the callback from the IDP and produce +// a JWT which we will store in a session cookie. By abstracting the +// provider we can easily test it. +type ProviderInterface interface { + GetRedirectURL(options []oauth2.AuthCodeOption, state string) string + GetJWT(ctx *HTTPClientContext, code string) (*http.Cookie, *Claims, error) +} + +// Compose the provider from various interfaces: +// The OidcRouter explains the different endpoints we need to access. + +// The ClaimsGetter is used to fetch claims from the server and decode +// the username from them. Velociraptor only cares about how to +// extract the username from the claims. +type Provider struct { + router OidcRouter + claims_getter ClaimsGetter + config_obj *config_proto.Config + oauth_config *oauth2.Config + authenticator *config_proto.Authenticator +} + +// Where to redirect to the +func (self *Provider) GetRedirectURL( + options []oauth2.AuthCodeOption, state string) string { + self.oauth_config.Endpoint = self.router.Endpoint() + return self.oauth_config.AuthCodeURL(state, options...) +} + +// GetJWT contacts the oauth server to fetch claims, creates a user +// object which is encoded in a JWT. The JWT can be verified on each +// subsequent request to ensure the user is logged on. +func (self *Provider) GetJWT( + ctx *HTTPClientContext, code string) (*http.Cookie, *Claims, error) { + + token, err := self.Exchange(ctx, self.oauth_config, code) + if err != nil { + return nil, nil, fmt.Errorf( + "can not get oauthToken from OIDC provider with code %v: %v", + code, err) + } + + claims, err := self.claims_getter.GetClaims(ctx, token) + if err != nil { + self.Debug("oauthOidcCallback: Unable to get claims: %v", err) + return nil, nil, err + } + + cookie, err := self.getSignedJWTTokenCookie(self.config_obj, claims) + if err != nil { + return nil, nil, err + } + + return cookie, claims, err +} + +func (self *Provider) getSignedJWTTokenCookie( + config_obj *config_proto.Config, + claims *Claims) (*http.Cookie, error) { + expiry_min := self.authenticator.DefaultSessionExpiryMin + if expiry_min == 0 { + expiry_min = 60 * 24 // 1 Day by default + } + + // We force expiry in the JWT **as well** as the session + // cookie. The JWT expiry is most important as the browser can + // replay session cookies past expiry. + expiry := utils.GetTime().Now().Add(time.Minute * time.Duration(expiry_min)) + + // Enforce the JWT to expire + claims.Expires = float64(expiry.Unix()) + + // Make a JWT and sign it. + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + self.Debug("getSignedJWTTokenCookie: Creating JWT with claims: %#v", claims) + + tokenString, err := token.SignedString([]byte(config_obj.Frontend.PrivateKey)) + if err != nil { + return nil, err + } + + // Sets the cookie on the browser so it is only valid from the + // base down. + return &http.Cookie{ + Name: "VelociraptorAuth", + Value: tokenString, + Path: api_utils.GetBaseDirectory(config_obj), + Secure: true, + HttpOnly: true, + Expires: expiry, + }, nil +} + +// Gets an oidc token from the server. +func (self *Provider) Exchange( + ctx context.Context, + oauth_config *oauth2.Config, code string) (*oauth2.Token, error) { + + oauth_config.Endpoint = self.router.Endpoint() + return oauth_config.Exchange(ctx, code) +} + +// Potentially log debug messages depending on the debug setting. +func (self *Provider) Debug(message string, args ...interface{}) { + if self.authenticator.OidcDebug { + logging.GetLogger(self.config_obj, &logging.GUIComponent). + Debug(message, args...) + } +} + +func NewProvider( + config_obj *config_proto.Config, + oauth_config *oauth2.Config, + authenticator *config_proto.Authenticator, + router OidcRouter, + claims_getter ClaimsGetter) (ProviderInterface, error) { + return &Provider{ + router: router, + config_obj: config_obj, + oauth_config: oauth_config, + authenticator: authenticator, + claims_getter: claims_getter, + }, nil +} diff --git a/api/authenticators/router.go b/api/authenticators/router.go new file mode 100644 index 000000000..408339048 --- /dev/null +++ b/api/authenticators/router.go @@ -0,0 +1,76 @@ +package authenticators + +import ( + oidc "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" +) + +// The router gives all the URLs to the relevant endpoints +type OidcRouter interface { + Name() string + LoginHandler() string + CallbackHandler() string + Scopes() []string + Issuer() string + Endpoint() oauth2.Endpoint + SetEndpoint(oauth2.Endpoint) + Avatar() string + + LoginURL() string +} + +type DefaultOidcRouter struct { + authenticator *config_proto.Authenticator + config_obj *config_proto.Config + endpoint oauth2.Endpoint +} + +func (self *DefaultOidcRouter) Name() string { + name := self.authenticator.OidcName + if name == "" { + return "Generic OIDC Connector" + } + return name +} + +func (self *DefaultOidcRouter) LoginHandler() string { + name := self.authenticator.OidcName + if name != "" { + return api_utils.Join("/auth/oidc/", name, "/login") + } + return "/auth/oidc/login" +} + +func (self *DefaultOidcRouter) LoginURL() string { + return api_utils.PublicURL(self.config_obj, self.LoginHandler()) +} + +func (self *DefaultOidcRouter) CallbackHandler() string { + name := self.authenticator.OidcName + if name != "" { + return api_utils.Join("/auth/oidc/", name, "/callback") + } + return "/auth/oidc/callback" +} + +func (self *DefaultOidcRouter) Scopes() []string { + return []string{oidc.ScopeOpenID, "email"} +} + +func (self *DefaultOidcRouter) Issuer() string { + return self.authenticator.OidcIssuer +} + +func (self *DefaultOidcRouter) Endpoint() oauth2.Endpoint { + return self.endpoint +} + +func (self *DefaultOidcRouter) SetEndpoint(ep oauth2.Endpoint) { + self.endpoint = ep +} + +func (self *DefaultOidcRouter) Avatar() string { + return self.authenticator.Avatar +} diff --git a/api/authenticators/saml.go b/api/authenticators/saml.go index 8c09a21ef..9689cbb7a 100644 --- a/api/authenticators/saml.go +++ b/api/authenticators/saml.go @@ -5,49 +5,67 @@ import ( "fmt" "net/http" "net/url" + "time" + "github.com/Velocidex/ordereddict" "github.com/crewjam/saml/samlsp" "github.com/gorilla/csrf" - "github.com/sirupsen/logrus" "www.velocidex.com/golang/velociraptor/acls" + acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" api_proto "www.velocidex.com/golang/velociraptor/api/proto" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/constants" crypto_utils "www.velocidex.com/golang/velociraptor/crypto/utils" "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/logging" - "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" ) var samlMiddleware *samlsp.Middleware -type SamlAuthenticator struct{} +type SamlAuthenticator struct { + config_obj *config_proto.Config + user_attribute string + authenticator *config_proto.Authenticator + user_roles []string +} func (self *SamlAuthenticator) IsPasswordLess() bool { return true } -func (self *SamlAuthenticator) AddHandlers(config_obj *config_proto.Config, mux *http.ServeMux) error { - auther := config_obj.GUI.Authenticator - logger := logging.Manager.GetLogger(config_obj, &logging.GUIComponent) - key, err := crypto_utils.ParseRsaPrivateKeyFromPemStr([]byte(auther.SamlPrivateKey)) +func (self *SamlAuthenticator) RequireClientCerts() bool { + return false +} + +func (self *SamlAuthenticator) AuthRedirectTemplate() string { + return self.authenticator.AuthRedirectTemplate +} + +func (self *SamlAuthenticator) AddHandlers(mux *api_utils.ServeMux) error { + logger := logging.GetLogger(self.config_obj, &logging.GUIComponent) + key, err := crypto_utils.ParseRsaPrivateKeyFromPemStr([]byte( + self.authenticator.SamlPrivateKey)) if err != nil { - return err + return fmt.Errorf("SamlAuthenticator: %w", err) } - cert, err := crypto_utils.ParseX509CertFromPemStr([]byte(auther.SamlCertificate)) + cert, err := crypto_utils.ParseX509CertFromPemStr([]byte( + self.authenticator.SamlCertificate)) if err != nil { - return err + return fmt.Errorf("SamlAuthenticator: %w", err) } - idpMetadataURL, err := url.Parse(auther.SamlIdpMetadataUrl) + idpMetadataURL, err := url.Parse(self.authenticator.SamlIdpMetadataUrl) if err != nil { - return err + return fmt.Errorf("SamlAuthenticator: %w", err) } - rootURL, err := url.Parse(auther.SamlRootUrl) + rootURL, err := url.Parse(self.authenticator.SamlRootUrl) if err != nil { - return err + return fmt.Errorf("SamlAuthenticator: %w", err) } idpMetadata, err := samlsp.FetchMetadata( @@ -55,88 +73,250 @@ func (self *SamlAuthenticator) AddHandlers(config_obj *config_proto.Config, mux http.DefaultClient, *idpMetadataURL) if err != nil { - return err + return fmt.Errorf("SamlAuthenticator: %w", err) } - samlMiddleware, err = samlsp.New(samlsp.Options{ - IDPMetadata: idpMetadata, - URL: *rootURL, - Key: key, - Certificate: cert, - }) + opts := samlsp.Options{ + IDPMetadata: idpMetadata, + URL: *rootURL, + Key: key, + Certificate: cert, + AllowIDPInitiated: self.authenticator.SamlAllowIdpInitiated, + } + samlMiddleware, err = samlsp.New(opts) if err != nil { - return err + return fmt.Errorf("SamlAuthenticator: %w", err) + } + + expiry_min := self.authenticator.DefaultSessionExpiryMin + if expiry_min == 0 { + expiry_min = 60 * 24 // 1 Day by default } - mux.Handle("/saml/", samlMiddleware) + maxAge := time.Minute * time.Duration(expiry_min) + jwtSessionCodec := samlsp.DefaultSessionCodec(opts) + jwtSessionCodec.MaxAge = maxAge + cookieSessionProvider := samlsp.DefaultSessionProvider(opts) + cookieSessionProvider.MaxAge = maxAge + cookieSessionProvider.Codec = jwtSessionCodec + samlMiddleware.Session = cookieSessionProvider + + mux.Handle(api_utils.GetBasePath(self.config_obj, "/saml/"), + IpFilter(self.config_obj, samlMiddleware)) logger.Info("Authentication via SAML enabled") return nil } +func (self *SamlAuthenticator) AddLogoff(mux *api_utils.ServeMux) error { + installLogoff(self.config_obj, mux) + return nil +} + func (self *SamlAuthenticator) AuthenticateUserHandler( - config_obj *config_proto.Config, - parent http.Handler) http.Handler { + parent http.Handler, + permission acls.ACL_PERMISSION, +) http.Handler { reject_handler := samlMiddleware.RequireAccount(parent) - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-CSRF-Token", csrf.Token(r)) + logger := GetLoggingHandler(self.config_obj)(parent) + + return api_utils.HandlerFunc(parent, + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-CSRF-Token", csrf.Token(r)) + + session, err := samlMiddleware.Session.GetSession(r) + if session == nil || err != nil { + reject_handler.ServeHTTP(w, r) + return + } + + sa, ok := session.(samlsp.SessionWithAttributes) + if !ok { + reject_handler.ServeHTTP(w, r) + return + } + + username := sa.GetAttributes().Get(self.user_attribute) + + user_record, err := self.MaybeCreateUser(r.Context(), username, r.RemoteAddr) + if err != nil { + err := services.LogAudit(r.Context(), + self.config_obj, username, "Authorization Failed", + ordereddict.NewDict(). + Set("error", err). + Set("username", username). + Set("roles", self.user_roles). + Set("remote", r.RemoteAddr)) + if err != nil { + logger := logging.GetLogger(self.config_obj, &logging.GUIComponent) + logger.Error("Authorization failed %v %v %v", + username, err, r.RemoteAddr) + } + + http.Error(w, + fmt.Sprintf("authorization failed: %v", err), + http.StatusUnauthorized) + return + } + err = self.MaybeAssignRoles(r.Context(), username) + if err != nil { + err := services.LogAudit(r.Context(), + self.config_obj, username, "Role Assignment Failed", + ordereddict.NewDict(). + Set("username", username). + Set("roles", self.user_roles). + Set("remote", r.RemoteAddr)) + if err != nil { + logger := logging.GetLogger(self.config_obj, &logging.GUIComponent) + logger.Error("Role Assignment Failed %v %v", + username, r.RemoteAddr) + } + + http.Error(w, + fmt.Sprintf("authorization failed: role assignment failed: %v", err), + http.StatusUnauthorized) + return + } + + // Does the user have access to the specified org? + err = CheckOrgAccess(self.config_obj, r, user_record, permission) + if err != nil { + err := services.LogAudit(r.Context(), + self.config_obj, username, "authorization failed: user not registered and no saml_user_roles set", + ordereddict.NewDict(). + Set("username", username). + Set("roles", self.user_roles). + Set("remote", r.RemoteAddr). + Set("status", http.StatusUnauthorized)) + if err != nil { + logger := logging.GetLogger(self.config_obj, &logging.GUIComponent) + logger.Error("no saml_user_roles set %v %v", + username, r.RemoteAddr) + } - session, err := samlMiddleware.Session.GetSession(r) - if session == nil { - reject_handler.ServeHTTP(w, r) - return + http.Error(w, + fmt.Sprintf("authorization failed: user not registered - contact your system administrator: %v", err), + http.StatusUnauthorized) + return + } + + user_info := &api_proto.VelociraptorUser{ + Name: user_record.Name, + } + + serialized, _ := json.Marshal(user_info) + ctx := context.WithValue( + r.Context(), constants.GRPC_USER_CONTEXT, + string(serialized)) + logger.ServeHTTP(w, r.WithContext(ctx)) + }).AddChild("GetLoggingHandler") +} + +func (self *SamlAuthenticator) MaybeCreateUser(ctx context.Context, username string, remote string) (*api_proto.VelociraptorUser, error) { + user_manager := services.GetUserManager() + user_record, err := user_manager.GetUser(ctx, username, username) + + if utils.IsNotFound(err) { + // we only create users if the "user_roles" option is set + if len(self.user_roles) == 0 { + _ = services.LogAudit(ctx, + self.config_obj, username, "Authorization failed: no saml user roles assigned", + ordereddict.NewDict(). + Set("username", username). + Set("roles", self.user_roles). + Set("remote", remote)) + return nil, err } - sa, ok := session.(samlsp.SessionWithAttributes) - if !ok { - reject_handler.ServeHTTP(w, r) - return + user_record := &api_proto.VelociraptorUser{ + Name: username, + } + err = services.LogAudit(ctx, self.config_obj, username, + "Create User From SAML", + ordereddict.NewDict().Set("username", username).Set("roles", self.user_roles).Set("remote", remote)) + if err != nil { + return nil, err } - username := sa.GetAttributes().Get(userAttr(config_obj)) - user_record, err := users.GetUser(config_obj, username) - - perm, err2 := acls.CheckAccess(config_obj, username, acls.READ_RESULTS) - if err != nil || !perm || err2 != nil || - user_record.Locked || user_record.Name != username { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(http.StatusUnauthorized) - - fmt.Fprintf(w, ` - -Authorization failed. You are not registered on this system as %v. -Contact your system administrator to get an account, then try again. - -`, username) - - logging.GetLogger(config_obj, &logging.Audit). - WithFields(logrus.Fields{ - "user": username, - "remote": r.RemoteAddr, - "method": r.Method, - }).Error("User rejected by GUI") - - return + err = user_manager.SetUser(ctx, user_record) + if err != nil { + return nil, err } - user_info := &api_proto.VelociraptorUser{ - Name: username, + return user_record, nil + } else { + return user_record, err + } +} + +func (self *SamlAuthenticator) MaybeAssignRoles( + ctx context.Context, + username string, +) error { + if len(self.user_roles) == 0 { + return nil + } + + // Usually roles are set per org but setting roles through the + // SAML IDP will grant the roles on all orgs. + org_manager, err := services.GetOrgManager() + if err != nil { + return err + } + + for _, org := range org_manager.ListOrgs() { + org_config_obj, err := org_manager.GetOrgConfig(org.Id) + if err != nil { + continue + } + + // Get the user's ACL policy in that org + existing_acls, err := services.GetPolicy(org_config_obj, username) + if err != nil { + // If a user does not exist this will fail to get their + // policy so start with a fresh policy. + existing_acls = &acl_proto.ApiClientACL{} + } + + new_roles := append([]string{}, existing_acls.Roles...) + // Add new roles + for _, role := range self.user_roles { + if !utils.InString(new_roles, role) { + new_roles = append(new_roles, role) + } } - serialized, _ := json.Marshal(user_info) - ctx := context.WithValue( - r.Context(), constants.GRPC_USER_CONTEXT, - string(serialized)) - GetLoggingHandler(config_obj)(parent).ServeHTTP( - w, r.WithContext(ctx)) - return - }) + // Only set the roles if we need to + if len(new_roles) > len(existing_acls.Roles) { + err = services.LogAudit(ctx, self.config_obj, username, + "Grant User Role From SAML", + ordereddict.NewDict(). + Set("Roles", new_roles). + Set("OrgId", org.Id)) + if err != nil { + continue + } + err = services.GrantRoles(org_config_obj, username, new_roles) + if err != nil { + return err + } + } + } + return nil } -func userAttr(config_obj *config_proto.Config) string { - auther := config_obj.GUI.Authenticator - if auther.SamlUserAttribute == "" { - return "name" +func NewSamlAuthenticator( + config_obj *config_proto.Config, + auther *config_proto.Authenticator) (*SamlAuthenticator, error) { + result := &SamlAuthenticator{ + config_obj: config_obj, + user_attribute: "name", + authenticator: auther, + user_roles: auther.SamlUserRoles, + } + + if auther.SamlUserAttribute != "" { + result.user_attribute = auther.SamlUserAttribute } - return auther.SamlUserAttribute + return result, nil } diff --git a/api/authenticators/template.go b/api/authenticators/template.go new file mode 100644 index 000000000..9a54a3b07 --- /dev/null +++ b/api/authenticators/template.go @@ -0,0 +1,87 @@ +package authenticators + +import ( + "net/http" + "strings" + "text/template" + + api_utils "www.velocidex.com/golang/velociraptor/api/utils" + utils "www.velocidex.com/golang/velociraptor/api/utils" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/gui/velociraptor" + gui_assets "www.velocidex.com/golang/velociraptor/gui/velociraptor" + "www.velocidex.com/golang/velociraptor/json" +) + +func renderRejectionMessage( + config_obj *config_proto.Config, + r *http.Request, w http.ResponseWriter, err error, + username string, authenticators []velociraptor.AuthenticatorInfo) { + + // For API calls we render the error as JSON + base_path := api_utils.GetBasePath(config_obj, "/api/") + if strings.HasPrefix(r.URL.Path, base_path) { + _, _ = w.Write([]byte(json.Format(`{"message": %q}`, err.Error()))) + return + } + + data, err := gui_assets.ReadFile("/index.html") + if err != nil { + //utils.Debug(err) + w.WriteHeader(500) + return + } + + tmpl, err := template.New("").Parse(string(data)) + if err != nil { + //utils.Debug(err) + w.WriteHeader(500) + return + } + + err = tmpl.Execute(w, velociraptor.HTMLtemplateArgs{ + BasePath: utils.GetBasePath(config_obj), + ErrState: json.MustMarshalString(velociraptor.ErrState{ + Type: "Login", + Username: username, + Authenticators: authenticators, + BasePath: utils.GetBasePath(config_obj), + }), + }) + if err != nil { + //utils.Debug(err) + w.WriteHeader(500) + } +} + +func renderLogoffMessage( + config_obj *config_proto.Config, + w http.ResponseWriter, username string) { + data, err := gui_assets.ReadFile("/index.html") + if err != nil { + //utils.Debug(err) + w.WriteHeader(500) + return + } + + tmpl, err := template.New("").Parse(string(data)) + if err != nil { + //utils.Debug(err) + w.WriteHeader(500) + return + } + + err = tmpl.Execute(w, velociraptor.HTMLtemplateArgs{ + BasePath: utils.GetBasePath(config_obj), + ErrState: json.MustMarshalString(velociraptor.ErrState{ + Type: "Logoff", + Username: username, + BasePath: utils.GetBaseDirectory(config_obj), + Authenticators: []velociraptor.AuthenticatorInfo{}, + }), + }) + if err != nil { + //utils.Debug(err) + w.WriteHeader(500) + } +} diff --git a/api/authenticators/users.go b/api/authenticators/users.go new file mode 100644 index 000000000..52f4967ad --- /dev/null +++ b/api/authenticators/users.go @@ -0,0 +1,18 @@ +package authenticators + +import ( + "context" + + "www.velocidex.com/golang/velociraptor/services" +) + +// Try to store the picture URL in the datastore to avoid making the +// cookie too large. Not critical if it fails, just move on. +func setUserPicture(ctx context.Context, username, url string) { + user_manager := services.GetUserManager() + user_record, err := user_manager.GetUserWithHashes(ctx, username, username) + if err == nil && user_record.Picture != url { + user_record.Picture = url + _ = user_manager.SetUser(ctx, user_record) + } +} diff --git a/api/authenticators/verify.go b/api/authenticators/verify.go new file mode 100644 index 000000000..1e33a799c --- /dev/null +++ b/api/authenticators/verify.go @@ -0,0 +1,239 @@ +package authenticators + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/Velocidex/ordereddict" + jwt "github.com/golang-jwt/jwt/v4" + "github.com/gorilla/csrf" + "www.velocidex.com/golang/velociraptor/acls" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/gui/velociraptor" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/services" + utils "www.velocidex.com/golang/velociraptor/utils" +) + +var ( + reauthError = errors.New(`Authentication cookie not found, invalid or expired. +You probably need to re-authenticate in a new tab or refresh this page.`) +) + +// Middleware function to enforce the request is authenticated. +// 1. Extract the claims from the JWT cookie +// 2. Make sure the user has the required permission on the specified org. +// 3. If user is authorized we pass a token to the gRPC gateway so it +// can become available inside the API server. This way the HTTP +// handler can identify the user, and pass that fact to the API +// backend without needing to re-auth the user again. +func authenticateUserHandle( + config_obj *config_proto.Config, + permission acls.ACL_PERMISSION, + reject_cb func(w http.ResponseWriter, r *http.Request, + err error, username string), + parent http.Handler) http.Handler { + + logger := GetLoggingHandler(config_obj)(parent) + + return api_utils.HandlerFunc(parent, + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-CSRF-Token", csrf.Token(r)) + + claims, err := getDetailsFromCookie(config_obj, r) + if err != nil { + reject_cb(w, r, err, claims.Username) + return + } + + username := claims.Username + + // Now check if the user is allowed to log in. + users := services.GetUserManager() + user_record, err := users.GetUser(r.Context(), username, username) + if err != nil { + reject_cb(w, r, fmt.Errorf("Invalid user: %v", err), username) + return + } + + // Does the user have access to the specified org? + err = CheckOrgAccess(config_obj, r, user_record, permission) + if err != nil { + reject_cb(w, r, fmt.Errorf("Insufficient permissions: %v", err), user_record.Name) + return + } + + // Checking is successful - user authorized. Here we + // build a token to pass to the underlying GRPC + // service with metadata about the user. + user_info := &api_proto.VelociraptorUser{ + Name: user_record.Name, + } + + // NOTE: This context is NOT the same context that is received + // by the API handlers. This context sits on the incoming side + // of the GRPC gateway. We stuff our data into the + // GRPC_USER_CONTEXT of the context and the code will convert + // this value into a GRPC metadata. + + // Must use json encoding because grpc can not handle + // binary data in metadata. + serialized, _ := json.Marshal(user_info) + ctx := context.WithValue( + r.Context(), constants.GRPC_USER_CONTEXT, string(serialized)) + + _ = users.SetUserStats(r.Context(), config_obj, username, + &api_proto.UserStats{ + LastActiveTime: utils.GetTime().Now().Unix(), + LastIpAddress: r.RemoteAddr, + }) + + // Need to call logging after auth so it can access + // the contextKeyUser value in the context. + logger.ServeHTTP(w, r.WithContext(ctx)) + }).AddChild("GetLoggingHandler") +} + +// Reject the user with a message and also add to the audit log. +func reject_with_username( + config_obj *config_proto.Config, + w http.ResponseWriter, r *http.Request, + err error, username, login_url, provider, avatar string) { + + // Log failed login to the audit log only if there is an actual + // user. First redirect will have username blank. + if username != "" { + err := services.LogAudit(r.Context(), + config_obj, username, "User rejected by GUI", + ordereddict.NewDict(). + Set("remote", r.RemoteAddr). + Set("method", r.Method). + Set("url", r.URL.String()). + Set("err", err.Error())) + if err != nil { + logger := logging.GetLogger( + config_obj, &logging.FrontendComponent) + logger.Error("LogAudit: User rejected by GUI %v %v", + username, r.RemoteAddr) + } + + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusUnauthorized) + + renderRejectionMessage(config_obj, + r, w, err, username, []velociraptor.AuthenticatorInfo{ + { + LoginURL: api_utils.PublicURL(config_obj, login_url), + ProviderAvatar: avatar, + ProviderName: provider, + }, + }) +} + +// Create a new JWT cookie embedding the claims in it. +// The JWT is signed with the server's private key so we can verify it +// easily and it can not be modified. +func getSignedJWTTokenCookie( + config_obj *config_proto.Config, + authenticator *config_proto.Authenticator, + claims *Claims, r *http.Request) (*http.Cookie, error) { + if config_obj.Frontend == nil { + return nil, errors.New("config has no Frontend") + } + + expiry_min := authenticator.DefaultSessionExpiryMin + if expiry_min == 0 { + expiry_min = 60 * 24 // 1 Day by default + } + + // We force expiry in the JWT **as well** as the session + // cookie. The JWT expiry is most important as the browser can + // replay session cookies past expiry. + expiry := utils.GetTime().Now().Add(time.Minute * time.Duration(expiry_min)) + + // Enforce the JWT to expire + claims.Expires = float64(expiry.Unix()) + + // Make a JWT and sign it. + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + if authenticator.OidcDebug { + logging.GetLogger(config_obj, &logging.GUIComponent). + Debug("getSignedJWTTokenCookie: Creating JWT with claims: %#v", claims) + } + + tokenString, err := token.SignedString([]byte(config_obj.Frontend.PrivateKey)) + if err != nil { + return nil, err + } + + // Log a successful login. + err = services.LogAudit(r.Context(), + config_obj, claims.Username, "Login", + ordereddict.NewDict(). + Set("remote", r.RemoteAddr). + Set("authenticator", authenticator.Type). + Set("url", r.URL.Path)) + if err != nil { + logger := logging.GetLogger(config_obj, &logging.FrontendComponent) + logger.Error("getSignedJWTTokenCookie LogAudit: Login %v %v", + claims.Username, r.RemoteAddr) + } + + // Sets the cookie on the browser so it is only valid from the + // base down. + return &http.Cookie{ + Name: "VelociraptorAuth", + Value: tokenString, + Path: api_utils.GetBaseDirectory(config_obj), + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Expires: expiry, + }, nil +} + +// Extracts the claims from the VelociraptorAuth cookie: +// Ensure the JWT is properly validated and contains all the +// required fields. +func getDetailsFromCookie( + config_obj *config_proto.Config, + r *http.Request) (*Claims, error) { + + claims := &Claims{} + + // We store the user name and their details in a local + // cookie. It is stored as a JWT so we can trust it. + auth_cookie, err := r.Cookie("VelociraptorAuth") + if err != nil { + return claims, reauthError + } + + // Parse the JWT. + token, err := jwt.ParseWithClaims(auth_cookie.Value, claims, + func(token *jwt.Token) (interface{}, error) { + _, ok := token.Method.(*jwt.SigningMethodHMAC) + if !ok { + return claims, errors.New("invalid signing method") + } + return []byte(config_obj.Frontend.PrivateKey), nil + }) + if err != nil { + return claims, fmt.Errorf("%w: %v", err, reauthError.Error()) + } + + claims, ok := token.Claims.(*Claims) + if ok && token.Valid { + return claims, nil + } + + return claims, reauthError +} diff --git a/api/builder.go b/api/builder.go index cc3b6a701..f31a6ec4e 100644 --- a/api/builder.go +++ b/api/builder.go @@ -3,20 +3,23 @@ package api import ( "context" "crypto/tls" + "crypto/x509" "errors" "fmt" "net/http" "os" - "path/filepath" "sync" "sync/atomic" "time" "golang.org/x/crypto/acme/autocert" + "www.velocidex.com/golang/velociraptor/api/authenticators" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/server" "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" _ "www.velocidex.com/golang/velociraptor/result_sets/timed" ) @@ -54,7 +57,8 @@ func (self *Builder) StartServer(ctx context.Context, wg *sync.WaitGroup) error return err } - // Start in autocert mode, only put the GUI behind autocert if the GUI port is 443. + // Start in autocert mode, only put the GUI behind autocert if the + // GUI port is 443. if self.AutocertCertCache != "" && self.config_obj.GUI != nil && self.config_obj.GUI.BindPort == 443 { return self.WithAutocertGUI(ctx, wg) @@ -62,7 +66,8 @@ func (self *Builder) StartServer(ctx context.Context, wg *sync.WaitGroup) error // Start in autocert mode, but only sign the frontend. if self.AutocertCertCache != "" { - return self.withAutoCertFrontendSelfSignedGUI(ctx, wg, self.config_obj, self.server_obj) + return self.withAutoCertFrontendSelfSignedGUI( + ctx, wg, self.config_obj, self.server_obj) } // All services are sharing the same port. @@ -94,12 +99,6 @@ func NewServerBuilder(ctx context.Context, return result, nil } -func (self *Builder) Close() { - if self.server_obj != nil { - self.server_obj.Close() - } -} - func (self *Builder) WithAPIServer(ctx context.Context, wg *sync.WaitGroup) error { return startAPIServer(ctx, wg, self.config_obj, self.server_obj) } @@ -117,8 +116,8 @@ func (self *Builder) withAutoCertFrontendSelfSignedGUI( logger := logging.GetLogger(config_obj, &logging.GUIComponent) logger.Info("Autocert is enabled but GUI port is not 443, starting Frontend with autocert and GUI with self signed.") - if config_obj.Frontend.ServerServices.GuiServer && config_obj.GUI != nil { - mux := http.NewServeMux() + if config_obj.Services.GuiServer && config_obj.GUI != nil { + mux := api_utils.NewServeMux() router, err := PrepareGUIMux(ctx, config_obj, mux) if err != nil { @@ -136,11 +135,15 @@ func (self *Builder) withAutoCertFrontendSelfSignedGUI( } } + if !config_obj.Services.FrontendServer { + return nil + } + // Launch a server for the frontend. - mux := http.NewServeMux() + mux := api_utils.NewServeMux() err := server.PrepareFrontendMux( - config_obj, server_obj, mux) + config_obj, server_obj, mux.ServeMux) if err != nil { return err } @@ -160,11 +163,14 @@ func (self *Builder) WithAutocertGUI( return errors.New("Frontend not configured") } - mux := http.NewServeMux() + mux := api_utils.NewServeMux() - err := server.PrepareFrontendMux(self.config_obj, self.server_obj, mux) - if err != nil { - return err + if self.config_obj.Services.FrontendServer { + err := server.PrepareFrontendMux( + self.config_obj, self.server_obj, mux.ServeMux) + if err != nil { + return err + } } router, err := PrepareGUIMux(ctx, self.config_obj, mux) @@ -184,16 +190,20 @@ func startSharedSelfSignedFrontend( wg *sync.WaitGroup, config_obj *config_proto.Config, server_obj *server.Server) error { - mux := http.NewServeMux() + mux := api_utils.NewServeMux() if config_obj.Frontend == nil || config_obj.GUI == nil { return errors.New("Frontend not configured") } - err := server.PrepareFrontendMux(config_obj, server_obj, mux) - if err != nil { - return err + if config_obj.Services.FrontendServer { + err := server.PrepareFrontendMux( + config_obj, server_obj, mux.ServeMux) + if err != nil { + return err + } } + router, err := PrepareGUIMux(ctx, config_obj, mux) if err != nil { return err @@ -201,12 +211,30 @@ func startSharedSelfSignedFrontend( // Combine both frontend and GUI on HTTP server. if config_obj.GUI.UsePlainHttp && config_obj.Frontend.UsePlainHttp { + server_obj.Info("Frontend and GUI both share port with plain HTTP %v", + config_obj.Frontend.BindPort) + return StartFrontendPlainHttp( ctx, wg, config_obj, server_obj, mux) } - return StartFrontendHttps(ctx, wg, - config_obj, server_obj, router) + server_obj.Info("Frontend and GUI both share port %v", + config_obj.Frontend.BindPort) + + auther, err := authenticators.NewAuthenticator(config_obj) + if err != nil { + return err + } + + if config_obj.Frontend.RequireClientCertificates != auther.RequireClientCerts() { + return errors.New( + "When using configurations that place the Frontend and GUI on the same port and requiring mTLS client certificates, then the GUI must also use the client certificate authenticator. Either split the frotnend and GUI on different ports or use the ClientCertificate authenticator.") + } + + if config_obj.Frontend.RequireClientCertificates { + server_obj.Info("Frontend and GUI will both require mTLS client side certificates!") + } + return StartFrontendHttps(ctx, wg, config_obj, server_obj, router) } // Start the Frontend and GUI on different ports using different @@ -217,13 +245,13 @@ func startSelfSignedFrontend( config_obj *config_proto.Config, server_obj *server.Server) error { - if config_obj.Frontend == nil { + if config_obj.Services == nil { return errors.New("Frontend not configured") } // Launch a new server for the GUI. - if config_obj.Frontend.ServerServices.GuiServer { - mux := http.NewServeMux() + if config_obj.Services.GuiServer { + mux := api_utils.NewServeMux() router, err := PrepareGUIMux(ctx, config_obj, mux) if err != nil { @@ -241,11 +269,17 @@ func startSelfSignedFrontend( } } + if !config_obj.Services.FrontendServer { + return nil + } + // Launch a server for the frontend. - mux := http.NewServeMux() + mux := api_utils.NewServeMux() - server.PrepareFrontendMux( - config_obj, server_obj, mux) + err := server.PrepareFrontendMux(config_obj, server_obj, mux.ServeMux) + if err != nil { + return err + } if config_obj.Frontend.UsePlainHttp { return StartFrontendPlainHttp( @@ -291,11 +325,20 @@ func StartFrontendHttps( return errors.New("Frontend server not configured") } - certs, err := getCertificates(config_obj) + tls_config := &tls.Config{} + err := getTLSConfig(config_obj, tls_config) if err != nil { return err } + if config_obj.Frontend.RequireClientCertificates { + err = addClientCerts(config_obj, tls_config) + if err != nil { + return err + } + server_obj.Info("Frontend will require mTLS client side certificates!") + } + listenAddr := fmt.Sprintf( "%s:%d", config_obj.Frontend.BindAddress, @@ -310,22 +353,7 @@ func StartFrontendHttps( ReadTimeout: 500 * time.Second, WriteTimeout: 900 * time.Second, IdleTimeout: 150 * time.Second, - TLSConfig: &tls.Config{ - MinVersion: tls.VersionTLS12, - Certificates: certs, - CurvePreferences: []tls.CurveID{tls.CurveP521, - tls.CurveP384, tls.CurveP256}, - - PreferServerCipherSuites: true, - CipherSuites: []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_RSA_WITH_AES_128_GCM_SHA256, - }, - }, + TLSConfig: tls_config, } wg.Add(1) @@ -339,14 +367,18 @@ func StartFrontendHttps( listener, err, closer := server_obj.NewLoadSheddingListener(server.Addr) if err != nil { - server_obj.Fatal("Frontend server: Can not listen on "+server.Addr, - err) + server_obj.Error("Frontend server: Can not listen on %v: %v", + server.Addr, err) + return } - defer closer() + defer func() { + _ = closer() + }() err = server.ServeTLS(listener, "", "") if err != nil && err != http.ErrServerClosed { - server_obj.Fatal("Frontend server error %v", err) + server_obj.Error("Frontend server error %v", err) + return } }() @@ -358,8 +390,9 @@ func StartFrontendHttps( server_obj.Info("Shutting down frontend") atomic.StoreInt32(&server_obj.Healthy, 0) - time_ctx, cancel := context.WithTimeout( - context.Background(), 10*time.Second) + time_ctx, cancel := utils.WithTimeoutCause( + context.Background(), 10*time.Second, + errors.New("Deadline exceeded shuttin down frontend")) defer cancel() server.SetKeepAlivesEnabled(false) @@ -411,7 +444,8 @@ func StartFrontendPlainHttp( err := server.ListenAndServe() if err != nil && err != http.ErrServerClosed { - server_obj.Fatal("Frontend server error %v", err) + server_obj.Error("Frontend server error %v", err) + return } }() @@ -423,15 +457,8 @@ func StartFrontendPlainHttp( server_obj.Info("Shutting down frontend") atomic.StoreInt32(&server_obj.Healthy, 0) - time_ctx, cancel := context.WithTimeout( - context.Background(), 10*time.Second) - defer cancel() - server.SetKeepAlivesEnabled(false) - err := server.Shutdown(time_ctx) - if err != nil { - server_obj.Error("Frontend server error %v", err) - } + _ = server.Shutdown(ctx) }() return nil @@ -450,25 +477,60 @@ func StartFrontendWithAutocert( return errors.New("Frontend server not configured") } - logger := logging.Manager.GetLogger(config_obj, &logging.GUIComponent) + logger := logging.GetLogger(config_obj, &logging.GUIComponent) // Autocert directory must be unique since it is usually kept in // shared storage. cache_dir := config_obj.AutocertCertCache if config_obj.Frontend.IsMinion { - cache_dir = filepath.Join( - cache_dir, services.GetNodeName(config_obj.Frontend)) + cache_dir = utils.Join(cache_dir, services.GetNodeName(config_obj.Frontend)) err := os.MkdirAll(cache_dir, 0700) if err != nil { return err } } + certManager := autocert.Manager{ Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist(config_obj.Frontend.Hostname), Cache: autocert.DirCache(cache_dir), } + tls_config := &tls.Config{} + err := getTLSConfig(config_obj, tls_config) + if err != nil { + return err + } + + auther, err := authenticators.NewAuthenticator(config_obj) + if err != nil { + return err + } + + // The frontend can not work with client certs required, so if we + // are in autocert mode we need either both frontend and gui to be + // configured with client cert or neither. + if config_obj.Frontend.RequireClientCertificates != auther.RequireClientCerts() { + return errors.New( + "When using configurations that place the Frontend and GUI on the same port and requiring mTLS client certificates, then the GUI must also use the client certificate authenticator. Either split the Frotnend and GUI on different ports or use the ClientCertificate authenticator.") + } + + if auther.RequireClientCerts() { + err = addClientCerts(config_obj, tls_config) + if err != nil { + return err + } + + server_obj.Info("Frontend and GUI will require mTLS client side certificates!") + } + + // Autocert selects its own certificates by itself + // https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.5.0:acme/autocert/autocert.go;l=227 + cert_manager_config := certManager.TLSConfig() + tls_config.GetCertificate = cert_manager_config.GetCertificate + tls_config.NextProtos = cert_manager_config.NextProtos + tls_config.Certificates = nil + server := &http.Server{ // ACME protocol requires TLS be served over port 443. Addr: ":https", @@ -479,14 +541,14 @@ func StartFrontendWithAutocert( ReadTimeout: 500 * time.Second, WriteTimeout: 900 * time.Second, IdleTimeout: 300 * time.Second, - TLSConfig: certManager.TLSConfig(), + TLSConfig: tls_config, } // We must have port 80 open to serve the HTTP 01 challenge. go func() { err := http.ListenAndServe(":http", certManager.HTTPHandler(nil)) if err != nil { - logger := logging.Manager.GetLogger(config_obj, &logging.GUIComponent) + logger := logging.GetLogger(config_obj, &logging.GUIComponent) logger.Error("Failed to bind to http server: %v", err) } }() @@ -504,14 +566,18 @@ func StartFrontendWithAutocert( // makes sense? listener, err, closer := server_obj.NewLoadSheddingListener(server.Addr) if err != nil { - server_obj.Fatal("Frontend server: Can not listen on "+server.Addr, - err) + server_obj.Error("Frontend server: Can not listen on %v: %v", + server.Addr, err) + return } - defer closer() + defer func() { + _ = closer() + }() err = server.ServeTLS(listener, "", "") if err != nil && err != http.ErrServerClosed { - server_obj.Fatal("Frontend server error", err) + server_obj.Error("Frontend server error: %v", err) + return } }() @@ -523,8 +589,9 @@ func StartFrontendWithAutocert( server_obj.Info("Stopping Frontend Server") atomic.StoreInt32(&server_obj.Healthy, 0) - timeout_ctx, cancel := context.WithTimeout( - context.Background(), 10*time.Second) + timeout_ctx, cancel := utils.WithTimeoutCause( + context.Background(), 10*time.Second, + errors.New("Deadline exceeded shuttin down frontend")) defer cancel() server.SetKeepAlivesEnabled(false) @@ -547,7 +614,7 @@ func StartHTTPGUI( return errors.New("GUI server not configured") } - logger := logging.Manager.GetLogger(config_obj, &logging.GUIComponent) + logger := logging.GetLogger(config_obj, &logging.GUIComponent) listenAddr := fmt.Sprintf("%s:%d", config_obj.GUI.BindAddress, @@ -584,8 +651,9 @@ func StartHTTPGUI( <-ctx.Done() logger.Info("Stopping GUI Server") - timeout_ctx, cancel := context.WithTimeout( - context.Background(), 10*time.Second) + timeout_ctx, cancel := utils.WithTimeoutCause( + context.Background(), 10*time.Second, + errors.New("Deadline exceeded shuttin down GUI")) defer cancel() server.SetKeepAlivesEnabled(false) @@ -602,16 +670,31 @@ func StartSelfSignedGUI( ctx context.Context, wg *sync.WaitGroup, config_obj *config_proto.Config, mux http.Handler) error { - logger := logging.Manager.GetLogger(config_obj, &logging.GUIComponent) + logger := logging.GetLogger(config_obj, &logging.GUIComponent) if config_obj.GUI == nil { return errors.New("GUI server not configured") } - certs, err := getCertificates(config_obj) + tls_config := &tls.Config{} + err := getTLSConfig(config_obj, tls_config) if err != nil { return err } + // If we are using an authenticator that requires client side + // certs, add the required TLS config here. + auther, err := authenticators.NewAuthenticator(config_obj) + if err != nil { + return err + } + + if auther.RequireClientCerts() { + err = addClientCerts(config_obj, tls_config) + if err != nil { + return err + } + } + listenAddr := fmt.Sprintf("%s:%d", config_obj.GUI.BindAddress, config_obj.GUI.BindPort) @@ -625,21 +708,7 @@ func StartSelfSignedGUI( ReadTimeout: 500 * time.Second, WriteTimeout: 900 * time.Second, IdleTimeout: 15 * time.Second, - TLSConfig: &tls.Config{ - MinVersion: tls.VersionTLS12, - CurvePreferences: []tls.CurveID{tls.CurveP521, - tls.CurveP384, tls.CurveP256}, - Certificates: certs, - PreferServerCipherSuites: true, - CipherSuites: []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_RSA_WITH_AES_128_GCM_SHA256, - }, - }, + TLSConfig: tls_config, } logger.Info("GUI is ready to handle TLS requests on https://%s:%d/", @@ -662,8 +731,9 @@ func StartSelfSignedGUI( <-ctx.Done() logger.Info("Stopping GUI Server") - timeout_ctx, cancel := context.WithTimeout( - context.Background(), 10*time.Second) + timeout_ctx, cancel := utils.WithTimeoutCause( + context.Background(), 10*time.Second, + errors.New("Deadline exceeded shuttin down GUI")) defer cancel() server.SetKeepAlivesEnabled(false) @@ -682,3 +752,70 @@ func get_hostname(fe_hostname, bind_addr string) string { } return bind_addr } + +func addClientCerts(config_obj *config_proto.Config, in *tls.Config) error { + // Require the browser to use client certificates + client_ca := x509.NewCertPool() + if config_obj.Client != nil { + client_ca.AppendCertsFromPEM([]byte(config_obj.Client.CaCertificate)) + + // Also trust any of our trusted root CAs. + if config_obj.Client.Crypto != nil && + config_obj.Client.Crypto.RootCerts != "" { + if !client_ca.AppendCertsFromPEM( + []byte(config_obj.Client.Crypto.RootCerts)) { + return errors.New( + "Unable to parse Crypto.root_certs in the config file.") + } + } + } + + in.ClientAuth = tls.RequireAndVerifyClientCert + in.ClientCAs = client_ca + + in.BuildNameToCertificate() + + return nil +} + +// Prepare a TLS config with correct cipher choices. +func getTLSConfig(config_obj *config_proto.Config, in *tls.Config) error { + certs, err := getCertificates(config_obj) + if err != nil { + return err + } + + expected_clients := int64(20000) + if config_obj.Frontend != nil { + if config_obj.Frontend.Resources != nil && + config_obj.Frontend.Resources.ExpectedClients > 0 { + expected_clients = config_obj.Frontend.Resources.ExpectedClients + } + } + + in.Certificates = certs + + // If the user requested it we loosen the TLS restrictions to + // accept default protocols. + if config_obj.Client != nil && config_obj.Client.Crypto != nil && + config_obj.Client.Crypto.AllowWeakTlsServer { + return nil + } + + in.MinVersion = tls.VersionTLS13 + in.CurvePreferences = []tls.CurveID{ + tls.CurveP521, tls.CurveP384, tls.CurveP256} + in.ClientSessionCache = tls.NewLRUClientSessionCache(int(expected_clients)) + in.PreferServerCipherSuites = true + + in.CipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + } + + return nil +} diff --git a/api/clients.go b/api/clients.go index 48423e4f6..c0ae666ac 100644 --- a/api/clients.go +++ b/api/clients.go @@ -1,19 +1,19 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package api @@ -21,147 +21,162 @@ import ( "context" "errors" "os" - "regexp" "time" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "github.com/Velocidex/ordereddict" "google.golang.org/protobuf/types/known/emptypb" "www.velocidex.com/golang/velociraptor/acls" api_proto "www.velocidex.com/golang/velociraptor/api/proto" - "www.velocidex.com/golang/velociraptor/datastore" - "www.velocidex.com/golang/velociraptor/flows" - flows_proto "www.velocidex.com/golang/velociraptor/flows/proto" - "www.velocidex.com/golang/velociraptor/paths" - "www.velocidex.com/golang/velociraptor/search" + "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" ) func (self *ApiServer) GetClientMetadata( ctx context.Context, in *api_proto.GetClientRequest) (*api_proto.ClientMetadata, error) { - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + defer Instrument("GetClientMetadata")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + user_name := user_record.Name permissions := acls.READ_RESULTS - if in.ClientId == "server" { + if in.ClientId == constants.VELOCIRAPTOR_SERVER_CLIENT_ID { permissions = acls.SERVER_ADMIN } - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, user_name, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view clients.") } - client_path_manager := paths.NewClientPathManager(in.ClientId) - db, err := datastore.GetDB(self.config) + client_info_manager, err := services.GetClientInfoManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) + } + + result := &api_proto.ClientMetadata{ + ClientId: in.ClientId, } - result := &api_proto.ClientMetadata{} - err = db.GetSubject(self.config, client_path_manager.Metadata(), result) + client_metadata, err := client_info_manager.GetMetadata(ctx, in.ClientId) if errors.Is(err, os.ErrNotExist) { // Metadata not set, start with empty set. err = nil } - return result, err + if err != nil { + return nil, Status(self.verbose, err) + } + + for _, i := range client_metadata.Items() { + result.Items = append(result.Items, + &api_proto.ClientMetadataItem{ + Key: i.Key, + Value: utils.ToString(i.Value), + }) + } + + return result, nil } func (self *ApiServer) SetClientMetadata( ctx context.Context, - in *api_proto.ClientMetadata) (*emptypb.Empty, error) { + in *api_proto.SetClientMetadataRequest) (*emptypb.Empty, error) { + + defer Instrument("SetClientMetadata")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + user_name := user_record.Name permissions := acls.LABEL_CLIENT - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, user_name, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to modify client labels.") } - client_path_manager := paths.NewClientPathManager(in.ClientId) - db, err := datastore.GetDB(self.config) + client_info_manager, err := services.GetClientInfoManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) + } + + metadata := ordereddict.NewDict() + for _, env := range in.Add { + metadata.Set(env.Key, env.Value) + } + + for _, key := range in.Remove { + _, pres := metadata.Get(key) + if !pres { + metadata.Set(key, nil) + } } - err = db.SetSubject(self.config, client_path_manager.Metadata(), in) - return &emptypb.Empty{}, err + err = client_info_manager.SetMetadata(ctx, in.ClientId, metadata, user_name) + return &emptypb.Empty{}, Status(self.verbose, err) } func (self *ApiServer) GetClient( ctx context.Context, in *api_proto.GetClientRequest) (*api_proto.ApiClient, error) { - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + defer Instrument("GetClient")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + user_name := user_record.Name permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, user_name, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view clients.") } + indexer, err := services.GetIndexer(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + // Update the user's MRU if in.UpdateMru { - err = search.UpdateMRU(self.config, user_name, in.ClientId) + err = indexer.UpdateMRU(org_config_obj, user_name, in.ClientId) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } } - api_client, err := search.FastGetApiClient(ctx, self.config, in.ClientId) + api_client, err := indexer.FastGetApiClient(ctx, org_config_obj, in.ClientId) if err != nil { - return nil, err + return &api_proto.ApiClient{}, nil } if self.server_obj != nil { - if !in.Lightweight && + if !in.Lightweight { // Wait up to 2 seconds to find out if clients are connected. - services.GetNotifier().IsClientConnected(ctx, - self.config, in.ClientId, 2) { - api_client.LastSeenAt = uint64(time.Now().UnixNano() / 1000) - } - } - - return api_client, nil -} - -func (self *ApiServer) GetClientFlows( - ctx context.Context, - in *api_proto.ApiFlowRequest) (*api_proto.ApiFlowResponse, error) { - - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) - if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, - "User is not allowed to view flows.") - } - - filter := func(flow *flows_proto.ArtifactCollectorContext) bool { - return true - } - - if in.Artifact != "" { - regex, err := regexp.Compile(in.Artifact) - if err != nil { - return nil, err - } - - filter = func(flow *flows_proto.ArtifactCollectorContext) bool { - if flow.Request == nil { - return false + notifier, err := services.GetNotifier(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) } - - for _, name := range flow.Request.Artifacts { - if regex.MatchString(name) { - return true - } + if notifier.IsClientConnected(ctx, + org_config_obj, in.ClientId, 2) { + api_client.LastSeenAt = uint64(time.Now().UnixNano() / 1000) } - return false } } - return flows.GetFlows(self.config, in.ClientId, - in.IncludeArchived, filter, in.Offset, in.Count) + + return api_client, nil } diff --git a/api/csrf.go b/api/csrf.go index a1e59851a..c8eea0570 100644 --- a/api/csrf.go +++ b/api/csrf.go @@ -6,8 +6,11 @@ import ( "os" "github.com/gorilla/csrf" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/services" ) // Wrap only a single handler with csrf protection. @@ -15,11 +18,11 @@ func csrfProtect(config_obj *config_proto.Config, parent http.Handler) http.Handler { // We may need to disabled CSRF for benchmarking tests. - disable_csrf, pres := os.LookupEnv("VELOCIRAPTOR_DISABLE_CSRF") + disable_csrf, pres := os.LookupEnv(constants.VELOCIRAPTOR_DISABLE_CSRF) if pres && disable_csrf == "1" { logger := logging.GetLogger(config_obj, &logging.GUIComponent) logger.Info("Disabling CSRF protection because environment VELOCIRAPTOR_DISABLE_CSRF is set") - return parent + return api_utils.HandlerFunc(parent, parent.ServeHTTP) } // Derive a CSRF key from the hash of the server's public key. @@ -27,9 +30,24 @@ func csrfProtect(config_obj *config_proto.Config, _, _ = hasher.Write([]byte(config_obj.Frontend.PrivateKey)) token := hasher.Sum(nil) - protectionFn := csrf.Protect(token, csrf.Path("/"), csrf.MaxAge(7*24*60*60)) + trusted_origins := append([]string{}, config_obj.GUI.TrustedOrigins...) + frontend_service, err := services.GetFrontendManager(config_obj) + if err == nil { + public_url, err := frontend_service.GetPublicUrl(config_obj) + if err == nil && public_url.Host != "" { + trusted_origins = append(trusted_origins, public_url.Host) + } + } + + protectionFn := csrf.Protect( + token, + csrf.Path("/"), + csrf.SameSite(csrf.SameSiteStrictMode), + csrf.TrustedOrigins(trusted_origins), + csrf.MaxAge(7*24*60*60)) - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - protectionFn(parent).ServeHTTP(w, r) - }) + return api_utils.HandlerFunc(parent, + func(w http.ResponseWriter, r *http.Request) { + protectionFn(parent).ServeHTTP(w, r) + }) } diff --git a/api/csv.go b/api/csv.go deleted file mode 100644 index 6e416f279..000000000 --- a/api/csv.go +++ /dev/null @@ -1,321 +0,0 @@ -/* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -package api - -import ( - "time" - - errors "github.com/pkg/errors" - context "golang.org/x/net/context" - "www.velocidex.com/golang/velociraptor/datastore" - file_store "www.velocidex.com/golang/velociraptor/file_store" - "www.velocidex.com/golang/velociraptor/file_store/api" - "www.velocidex.com/golang/velociraptor/file_store/csv" - "www.velocidex.com/golang/velociraptor/paths" - "www.velocidex.com/golang/velociraptor/paths/artifacts" - "www.velocidex.com/golang/velociraptor/result_sets" - "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/timelines" - - api_proto "www.velocidex.com/golang/velociraptor/api/proto" - artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" - config_proto "www.velocidex.com/golang/velociraptor/config/proto" -) - -func getTable( - ctx context.Context, - config_obj *config_proto.Config, - in *api_proto.GetTableRequest) ( - *api_proto.GetTableResponse, error) { - - rows := uint64(0) - if in.Rows == 0 { - in.Rows = 500 - } - - result := &api_proto.GetTableResponse{ - ColumnTypes: getColumnTypes(config_obj, in), - } - path_spec, err := getPathSpec(config_obj, in) - if err != nil { - return result, err - } - - file_store_factory := file_store.GetFileStore(config_obj) - rs_reader, err := result_sets.NewResultSetReader( - file_store_factory, path_spec) - if err != nil { - return result, nil - } - defer rs_reader.Close() - - // Let the browser know how many rows we have in total. - result.TotalRows = rs_reader.TotalRows() - - // FIXME: Backwards compatibility: Just give a few - // rows if the result set does not have an index. This - // is the same as the previous behavior but for new - // collections, an index is created and we respect the - // number of rows the callers asked for. Eventually - // this will not be needed. - if result.TotalRows < 0 { - in.Rows = 100 - } - - // Seek to the row we need. - err = rs_reader.SeekToRow(int64(in.StartRow)) - if err != nil { - return nil, err - } - - // Unpack the rows into the output protobuf - for row := range rs_reader.Rows(ctx) { - if result.Columns == nil { - result.Columns = row.Keys() - } - - row_data := make([]string, 0, len(result.Columns)) - for _, key := range result.Columns { - value, _ := row.Get(key) - row_data = append(row_data, csv.AnyToString(value)) - } - result.Rows = append(result.Rows, &api_proto.Row{ - Cell: row_data, - }) - - rows += 1 - if rows > in.Rows { - break - } - } - - return result, nil -} - -// The GUI is requesting table data. This function tries to figure out -// the column types. -func getColumnTypes( - config_obj *config_proto.Config, - in *api_proto.GetTableRequest) []*artifacts_proto.ColumnType { - - // For artifacts column types are specified in the `column_types` - // artifact definition. - if in.Artifact != "" { - manager, err := services.GetRepositoryManager() - if err != nil { - return nil - } - - repository, err := manager.GetGlobalRepository(config_obj) - if err != nil { - return nil - } - - artifact, pres := repository.Get(config_obj, in.Artifact) - if pres { - return artifact.ColumnTypes - } - } - - // For notebooks, the column_types are set in the notebook metadata. - if in.NotebookId != "" { - notebook_path_manager := paths.NewNotebookPathManager( - in.NotebookId) - - db, err := datastore.GetDB(config_obj) - if err != nil { - return nil - } - - notebook := &api_proto.NotebookMetadata{} - err = db.GetSubject(config_obj, notebook_path_manager.Path(), notebook) - if err == nil { - return notebook.ColumnTypes - } - } - - return nil -} - -func getPathSpec( - config_obj *config_proto.Config, - in *api_proto.GetTableRequest) (api.FSPathSpec, error) { - - if in.FlowId != "" && in.Artifact != "" { - path_manager, err := artifacts.NewArtifactPathManager( - config_obj, in.ClientId, in.FlowId, in.Artifact) - if err != nil { - return nil, err - } - return path_manager.Path(), nil - - } else if in.FlowId != "" && in.Type != "" { - flow_path_manager := paths.NewFlowPathManager( - in.ClientId, in.FlowId) - - switch in.Type { - case "log": - return flow_path_manager.Log(), nil - case "uploads": - return flow_path_manager.UploadMetadata(), nil - } - } else if in.HuntId != "" && in.Type == "clients" { - return paths.NewHuntPathManager(in.HuntId).Clients(), nil - - } else if in.HuntId != "" && in.Type == "hunt_status" { - return paths.NewHuntPathManager(in.HuntId).ClientErrors(), nil - - } else if in.NotebookId != "" && in.CellId != "" { - return paths.NewNotebookPathManager(in.NotebookId).Cell( - in.CellId).QueryStorage(in.TableId).Path(), nil - } - - return nil, errors.New("Invalid request") -} - -func getEventTable( - ctx context.Context, - config_obj *config_proto.Config, - in *api_proto.GetTableRequest) ( - *api_proto.GetTableResponse, error) { - path_manager, err := artifacts.NewArtifactPathManager( - config_obj, in.ClientId, in.FlowId, in.Artifact) - if err != nil { - return nil, err - } - - return getEventTableWithPathManager(ctx, config_obj, in, path_manager) -} - -func getEventTableLogs( - ctx context.Context, - config_obj *config_proto.Config, - in *api_proto.GetTableRequest) ( - *api_proto.GetTableResponse, error) { - path_manager, err := artifacts.NewArtifactLogPathManager( - config_obj, in.ClientId, "", in.Artifact) - if err != nil { - return nil, err - } - return getEventTableWithPathManager(ctx, config_obj, in, path_manager) -} - -func getEventTableWithPathManager( - ctx context.Context, - config_obj *config_proto.Config, - in *api_proto.GetTableRequest, - path_manager api.PathManager) ( - *api_proto.GetTableResponse, error) { - - rows := uint64(0) - if in.Rows == 0 { - in.Rows = 10 - } - - result := &api_proto.GetTableResponse{} - - file_store_factory := file_store.GetFileStore(config_obj) - rs_reader, err := result_sets.NewTimedResultSetReader(ctx, - file_store_factory, path_manager) - if err != nil { - return nil, err - } - defer rs_reader.Close() - - err = rs_reader.SeekToTime(time.Unix(int64(in.StartTime), 0)) - if err != nil { - return nil, err - } - - if in.EndTime != 0 { - rs_reader.SetMaxTime(time.Unix(int64(in.EndTime), 0)) - } - - // Unpack the rows into the output protobuf - for row := range rs_reader.Rows(ctx) { - if result.Columns == nil { - result.Columns = row.Keys() - } - - row_data := make([]string, 0, len(result.Columns)) - for _, key := range result.Columns { - value, _ := row.Get(key) - row_data = append(row_data, csv.AnyToString(value)) - } - result.Rows = append(result.Rows, &api_proto.Row{ - Cell: row_data, - }) - - rows += 1 - if rows > in.Rows { - break - } - } - - return result, nil -} - -func getTimeline( - ctx context.Context, - config_obj *config_proto.Config, - in *api_proto.GetTableRequest) (*api_proto.GetTableResponse, error) { - - if in.NotebookId == "" { - return nil, errors.New("NotebookId must be specified") - } - - path_manager := paths.NewNotebookPathManager(in.NotebookId). - SuperTimeline(in.Timeline) - reader, err := timelines.NewSuperTimelineReader( - config_obj, path_manager, in.SkipComponents) - if err != nil { - return nil, err - } - defer reader.Close() - - result := &api_proto.GetTableResponse{ - Columns: []string{"_Source", "Time", "Data"}, - StartTime: int64(in.StartTime), - } - - if in.StartTime != 0 { - ts := time.Unix(0, int64(in.StartTime)) - reader.SeekToTime(ts) - } - - rows := uint64(0) - for item := range reader.Read(ctx) { - if result.StartTime == 0 { - result.StartTime = item.Time.UnixNano() - } - result.EndTime = item.Time.UnixNano() - result.Rows = append(result.Rows, &api_proto.Row{ - Cell: []string{ - item.Source, - csv.AnyToString(item.Time), - csv.AnyToString(item.Row)}, - }) - - rows += 1 - if rows > in.Rows { - break - } - } - - return result, nil -} diff --git a/api/datastore.go b/api/datastore.go index 261c2ecf3..f6220c862 100644 --- a/api/datastore.go +++ b/api/datastore.go @@ -1,9 +1,9 @@ package api import ( + "context" "sync" - context "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" @@ -12,6 +12,7 @@ import ( "www.velocidex.com/golang/velociraptor/datastore" "www.velocidex.com/golang/velociraptor/file_store/api" "www.velocidex.com/golang/velociraptor/file_store/path_specs" + "www.velocidex.com/golang/velociraptor/services" ) // Raw Datastore access requires the DATASTORE_ACCESS permission. This @@ -21,16 +22,42 @@ func (self *ApiServer) GetSubject( ctx context.Context, in *api_proto.DataRequest) (*api_proto.DataResponse, error) { - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - perm, err := acls.CheckAccess(self.config, user_name, acls.DATASTORE_ACCESS) + defer Instrument("GetSubject")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + user_name := user_record.Name + token, err := services.GetEffectivePolicy(org_config_obj, user_name) + if err != nil { + return nil, Status(self.verbose, err) + } + + perm, err := services.CheckAccessWithToken(token, acls.DATASTORE_ACCESS) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to access datastore.") } - db, err := datastore.GetDB(self.config) + // Only the superuser is allowed to switch orgs. + if token.SuperUser && org_config_obj.OrgId != in.OrgId { + org_manager, err := services.GetOrgManager() + if err != nil { + return nil, Status(self.verbose, err) + } + + org_config_obj, err = org_manager.GetOrgConfig(in.OrgId) + if err != nil { + return nil, Status(self.verbose, err) + } + } + + db, err := datastore.GetDB(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } raw_db, ok := db.(datastore.RawDataStore) @@ -39,7 +66,7 @@ func (self *ApiServer) GetSubject( "Datastore has no raw access.") } - data, err := raw_db.GetBuffer(self.config, getURN(in)) + data, err := raw_db.GetBuffer(org_config_obj, getURN(in)) return &api_proto.DataResponse{ Data: data, }, err @@ -49,16 +76,42 @@ func (self *ApiServer) SetSubject( ctx context.Context, in *api_proto.DataRequest) (*api_proto.DataResponse, error) { - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - perm, err := acls.CheckAccess(self.config, user_name, acls.DATASTORE_ACCESS) + defer Instrument("SetSubject")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + user_name := user_record.Name + token, err := services.GetEffectivePolicy(org_config_obj, user_name) + if err != nil { + return nil, Status(self.verbose, err) + } + + perm, err := services.CheckAccessWithToken(token, acls.DATASTORE_ACCESS) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to access datastore.") } - db, err := datastore.GetDB(self.config) + // Only the superuser is allowed to switch orgs. + if token.SuperUser && org_config_obj.OrgId != in.OrgId { + org_manager, err := services.GetOrgManager() + if err != nil { + return nil, Status(self.verbose, err) + } + + org_config_obj, err = org_manager.GetOrgConfig(in.OrgId) + if err != nil { + return nil, Status(self.verbose, err) + } + } + + db, err := datastore.GetDB(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } raw_db, ok := db.(datastore.RawDataStore) @@ -72,7 +125,7 @@ func (self *ApiServer) SetSubject( // Wait for the data to hit the disk. wg.Add(1) - err = raw_db.SetBuffer(self.config, getURN(in), in.Data, func() { + err = raw_db.SetBuffer(org_config_obj, getURN(in), in.Data, func() { wg.Done() }) wg.Wait() @@ -80,7 +133,7 @@ func (self *ApiServer) SetSubject( } else { // Just write quickly. - err = raw_db.SetBuffer(self.config, getURN(in), in.Data, nil) + err = raw_db.SetBuffer(org_config_obj, getURN(in), in.Data, nil) } return &api_proto.DataResponse{}, err } @@ -89,21 +142,48 @@ func (self *ApiServer) ListChildren( ctx context.Context, in *api_proto.DataRequest) (*api_proto.ListChildrenResponse, error) { - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - perm, err := acls.CheckAccess(self.config, user_name, acls.DATASTORE_ACCESS) + defer Instrument("ListChildren")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + user_name := user_record.Name + token, err := services.GetEffectivePolicy(org_config_obj, user_name) + if err != nil { + return nil, Status(self.verbose, err) + } + + perm, err := services.CheckAccessWithToken(token, acls.DATASTORE_ACCESS) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to access datastore.") } - db, err := datastore.GetDB(self.config) + // The call can access the datastore from any org becuase it is a + // server->server call. + if token.SuperUser && org_config_obj.OrgId != in.OrgId { + org_manager, err := services.GetOrgManager() + if err != nil { + return nil, Status(self.verbose, err) + } + + org_config_obj, err = org_manager.GetOrgConfig(in.OrgId) + if err != nil { + return nil, Status(self.verbose, err) + } + } + + db, err := datastore.GetDB(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - children, err := db.ListChildren(self.config, getURN(in)) + children, err := db.ListChildren(org_config_obj, getURN(in)) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } result := &api_proto.ListChildrenResponse{} @@ -123,19 +203,43 @@ func (self *ApiServer) DeleteSubject( ctx context.Context, in *api_proto.DataRequest) (*emptypb.Empty, error) { - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - perm, err := acls.CheckAccess(self.config, user_name, acls.DATASTORE_ACCESS) + defer Instrument("DeleteSubject")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + user_name := user_record.Name + token, err := services.GetEffectivePolicy(org_config_obj, user_name) + if err != nil { + return nil, Status(self.verbose, err) + } + + perm, err := services.CheckAccessWithToken(token, acls.DATASTORE_ACCESS) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to access datastore.") } - db, err := datastore.GetDB(self.config) + if token.SuperUser && org_config_obj.OrgId != in.OrgId { + org_manager, err := services.GetOrgManager() + if err != nil { + return nil, Status(self.verbose, err) + } + + org_config_obj, err = org_manager.GetOrgConfig(in.OrgId) + if err != nil { + return nil, Status(self.verbose, err) + } + } + db, err := datastore.GetDB(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - return &emptypb.Empty{}, db.DeleteSubject(self.config, getURN(in)) + return &emptypb.Empty{}, db.DeleteSubject(org_config_obj, getURN(in)) } func getURN(in *api_proto.DataRequest) api.DSPathSpec { diff --git a/api/datastore_test.go b/api/datastore_test.go index 700dd8028..72a1cbf6d 100644 --- a/api/datastore_test.go +++ b/api/datastore_test.go @@ -1,12 +1,12 @@ -package api +package api_test import ( "testing" "time" - "github.com/sebdah/goldie" "github.com/stretchr/testify/suite" "google.golang.org/protobuf/proto" + "www.velocidex.com/golang/velociraptor/api" api_proto "www.velocidex.com/golang/velociraptor/api/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/datastore" @@ -16,6 +16,7 @@ import ( "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/vtesting" "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" ) type DatastoreAPITest struct { @@ -30,7 +31,7 @@ func (self *DatastoreAPITest) SetupTest() { self.TestSuite.SetupTest() - server_builder, err := NewServerBuilder( + server_builder, err := api.NewServerBuilder( self.Sm.Ctx, self.ConfigObj, self.Sm.Wg) assert.NoError(self.T(), err) @@ -38,15 +39,15 @@ func (self *DatastoreAPITest) SetupTest() { assert.NoError(self.T(), err) // Now bring up an API server. - self.ConfigObj.Frontend.ServerServices = &config_proto.ServerServicesConfig{} + self.ConfigObj.Services = &config_proto.ServerServicesConfig{} // Wait for the server to come up. - conn, closer, err := grpc_client.Factory.GetAPIClient( - self.Sm.Ctx, self.ConfigObj) - assert.NoError(self.T(), err) - defer closer() - vtesting.WaitUntil(2*time.Second, self.T(), func() bool { + conn, closer, err := grpc_client.Factory.GetAPIClient( + self.Sm.Ctx, grpc_client.SuperUser, self.ConfigObj) + assert.NoError(self.T(), err) + defer closer() + res, err := conn.Check(self.Sm.Ctx, &api_proto.HealthCheckRequest{}) return err == nil && res.Status == api_proto.HealthCheckResponse_SERVING }) @@ -63,7 +64,7 @@ func (self *DatastoreAPITest) TestDatastore() { // Make some RPC calls conn, closer, err := grpc_client.Factory.GetAPIClient( - self.Sm.Ctx, self.ConfigObj) + self.Sm.Ctx, grpc_client.SuperUser, self.ConfigObj) assert.NoError(self.T(), err) defer closer() diff --git a/api/docs.go b/api/docs.go new file mode 100644 index 000000000..3f38c12a8 --- /dev/null +++ b/api/docs.go @@ -0,0 +1,97 @@ +package api + +import ( + "context" + + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/services" +) + +/* + +# How does the Velociraptor server work? + +The Velociraptor server presents a GRPC API for manipulating and +presenting data. We chose gRPC because: + +1. It has mutual two way authentication - the server presents a + certificate to identify itself and the caller must also present a + properly signed certificate. + +2. Communication is encrypted using TLS + +3. Each RPC call can include a complex well defined API with protocol + buffers as inputs and outputs. + +4. GRPC has a streaming mode which is useful for real time + communications (e.g. via the Query API point). + +The server's API surface is well defined in api/proto/api.proto and +implemented in this "api" module. + +By tightening down the server api surface it is easier to ensure that +ACLs are properly enforced. + +## ACLs and permissions + +The API endpoints enforce the permission model based on the identity +of the caller. In the gRPC API the caller's identity is found by +examining the Common Name in the certificate that the user presented. + +The user identity is recovered using the users service +users.GetUserFromContext(ctx) + +## How is the GUI implemented? + +The GUI is a simple react app which communicates with the server using +AJAX calls, such as GET or POST. As such the GUI can not make direct +gRPC calls to the API server. + +To translate between REST calls to gRPC we use the grpc gateway +proxy. This proxy service exposes HTTP handlers on /api/ URLs. When a +HTTP connection occurs, the gateway proxy will bundle the data into +protocol buffers and make a proper gRPC call into the API. + +The gateway's gRPC connections are made using the gateway identity +(certificates generated in GUI.gw_certificate and +GUI.gw_private_key. The real identity of the calling user is injected +in the gRPC metadata channel under the "USER" parameter. + +From the API server's perspective, the user identity is: + +1. If the identity is not utils.GetGatewayName() then the identity is + fetched from the caller's X509 certificates. (After verifying the + certificates are issued by the internal CA) + +2. If the caller is really the Gateway, then the real identity of the + user is retrieved from the gRPC metadata "USER" variable (passed in + the context) + +This logic is implemented in services/users/grpc.go:GetGRPCUserInfo() + +NOTE: The gateway's certificates are critical to protect - if an actor + makes an API connection using these certificate they can just claim + to be anyone by injecting any username into the "USER" gRPC + metadata. + + +*/ + +func (self *ApiServer) SearchDocs( + ctx context.Context, + in *api_proto.DocSearchRequest) (*api_proto.DocSearchResponses, error) { + + users := services.GetUserManager() + _, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + // All users can search the docs with no permission required. + doc_manager, err := services.GetDocManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + return doc_manager.Search(ctx, in.Query, int(in.Start), int(in.Length)) +} diff --git a/api/download.go b/api/download.go index 7960c3e6e..02cadb90a 100644 --- a/api/download.go +++ b/api/download.go @@ -1,6 +1,6 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -26,9 +26,10 @@ package api import ( + "context" + "fmt" "html" "io" - "io/ioutil" "net/http" "net/url" "os" @@ -37,176 +38,386 @@ import ( "time" "github.com/Velocidex/ordereddict" + errors "github.com/go-errors/errors" "github.com/gorilla/schema" - errors "github.com/pkg/errors" - "github.com/sirupsen/logrus" - context "golang.org/x/net/context" + + file_store_accessor "www.velocidex.com/golang/velociraptor/accessors/file_store" + "www.velocidex.com/golang/velociraptor/acls" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" + "www.velocidex.com/golang/velociraptor/api/authenticators" api_proto "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/api/tables" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" - "www.velocidex.com/golang/velociraptor/datastore" + "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/file_store" "www.velocidex.com/golang/velociraptor/file_store/api" "www.velocidex.com/golang/velociraptor/file_store/csv" "www.velocidex.com/golang/velociraptor/file_store/path_specs" - "www.velocidex.com/golang/velociraptor/flows" - flows_proto "www.velocidex.com/golang/velociraptor/flows/proto" "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/paths" "www.velocidex.com/golang/velociraptor/paths/artifacts" + "www.velocidex.com/golang/velociraptor/reporting" "www.velocidex.com/golang/velociraptor/result_sets" "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/uploads" "www.velocidex.com/golang/velociraptor/utils" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" ) +const BUFSIZE = 1 * 1024 * 1024 + var ( pool = sync.Pool{ New: func() interface{} { - return make([]byte, 32*1024) + return make([]byte, BUFSIZE) }, } + + OkError = errors.New("Ok") + SparseFileError = errors.New("Sparse file is too sparse - unable to pad") + InvalidRequestError = utils.Wrap(utils.InvalidArgError, "Invalid request") ) -func returnError(w http.ResponseWriter, code int, message string) { +func returnError(config_obj *config_proto.Config, + w http.ResponseWriter, code int, err error) { + if errors.Is(err, utils.PermissionDenied) { + code = 403 + } + + message := "Error" + if config_obj.Verbose { + message = html.EscapeString(err.Error()) + } + w.WriteHeader(code) - _, _ = w.Write([]byte(html.EscapeString(message))) + _, _ = w.Write([]byte(message)) } type vfsFileDownloadRequest struct { - ClientId string `schema:"client_id"` - VfsPath string `schema:"vfs_path"` - Components []string `schema:"components[]"` - Offset int64 `schema:"offset"` - Length int `schema:"length"` - Encoding string `schema:"encoding"` + ClientId string `schema:"client_id"` + + // This is the path within the client VFS in the usual client path + // notation - this is what is seen in the uploads table. We use + // this field to determine the download attachment name. + VfsPath string `schema:"vfs_path"` + + // This is the file store path to fetch. + FSComponents []string `schema:"fs_components"` + Offset int64 `schema:"offset"` + Length int `schema:"length"` + OrgId string `schema:"org_id"` + + // The caller can specify we detect the mime type. Only a few + // types are supported. + DetectMime bool `schema:"detect_mime"` + + // If set we pad the file out. + Padding bool `schema:"padding"` + + // If set we filter binary chars to reveal only text + TextFilter bool `schema:"text_filter"` + Lines int `schema:"lines"` + + // Encapsulate the file in a zip file. + ZipFile bool `schema:"zip"` } // URL format: /api/v1/DownloadVFSFile // This URL allows the caller to download **any** member of the // filestore (providing they have at least read permissions). -func vfsFileDownloadHandler( - config_obj *config_proto.Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - request := vfsFileDownloadRequest{} - decoder := schema.NewDecoder() - err := decoder.Decode(&request, r.URL.Query()) - if err != nil { - returnError(w, 404, err.Error()) - return - } +func vfsFileDownloadHandler(config_obj *config_proto.Config) http.Handler { + return api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + request := vfsFileDownloadRequest{} + decoder := schema.NewDecoder() + decoder.IgnoreUnknownKeys(true) + + err := decoder.Decode(&request, r.URL.Query()) + if err != nil { + returnError(config_obj, w, 403, err) + return + } + + org_id := request.OrgId + if org_id == "" { + org_id = authenticators.GetOrgIdFromRequest(r) + } - var path_spec api.FSPathSpec - client_path_manager := paths.NewClientPathManager(request.ClientId) + org_id = utils.NormalizedOrgId(org_id) - // Uploads table has direct vfs paths - if request.VfsPath != "" { - path_spec, err = client_path_manager.GetUploadsFileFromVFSPath( - request.VfsPath) + org_manager, err := services.GetOrgManager() if err != nil { - returnError(w, 404, err.Error()) + returnError(config_obj, w, 404, err) return } - } else { - db, err := datastore.GetDB(config_obj) + org_config_obj, err := org_manager.GetOrgConfig(org_id) + if err != nil { + returnError(config_obj, w, 404, err) + return + } + + users := services.GetUserManager() + user_record, err := users.GetUserFromHTTPContext(r.Context()) if err != nil { - returnError(w, 404, err.Error()) + returnError(config_obj, w, 403, err) + return + } + principal := user_record.Name + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + returnError(config_obj, w, 403, utils.PermissionDenied) + return + } + + // Where to read from the file store + var path_spec api.FSPathSpec + + // The filename for the attachment header. + var filename string + + client_path_manager := paths.NewClientPathManager(request.ClientId) + + // Newer API calls pass the filestore components directly + if len(request.FSComponents) > 0 { + path_spec = path_specs.NewUnsafeFilestorePath(request.FSComponents...). + SetType(api.PATH_TYPE_FILESTORE_ANY) + + filename = utils.Base(request.VfsPath) + + // Uploads table has direct vfs paths + } else if request.VfsPath != "" { + path_spec, err = client_path_manager.GetUploadsFileFromVFSPath( + request.VfsPath) + if err != nil { + returnError(config_obj, w, 404, err) + return + } + filename = path_spec.Base() + + } else { + // Just reject the request + returnError(config_obj, w, 404, utils.PermissionDenied) return } - info_path_spec := client_path_manager.VFSDownloadInfoPath( - request.Components) - download_info := &flows_proto.VFSDownloadInfo{} + err = file_store_accessor.IsFileAccessible(path_spec) + if err != nil { + returnError(config_obj, w, 404, err) + return + } - err = db.GetSubject(config_obj, info_path_spec, download_info) + file, err := file_store.GetFileStore(org_config_obj). + ReadFile(path_spec) if err != nil { - returnError(w, 404, err.Error()) + returnError(config_obj, w, 404, err) return } - path_spec = path_specs.NewUnsafeFilestorePath( - download_info.Components...). - SetType(api.PATH_TYPE_FILESTORE_ANY) + defer file.Close() - } + if r.Method == "HEAD" { + returnError(config_obj, w, 200, OkError) + return + } - file, err := file_store.GetFileStore(config_obj).ReadFile(path_spec) - if err != nil { - returnError(w, 404, err.Error()) - return - } - defer file.Close() + // We need to figure out the total size of the upload to set + // in the Content Length header. There are three + // possibilities: + // 1. The file is not sparse + // 2. The file is sparse and we are not padding. + // 3. The file is sparse and we are padding it. + var reader_at io.ReaderAt = utils.MakeReaderAtter(file) + var total_size int + + index, err := getIndex(org_config_obj, path_spec) + + // If the file is sparse, we use the sparse reader. + if err == nil && request.Padding && len(index.Ranges) > 0 { + if !uploads.ShouldPadFile(org_config_obj, index) { + returnError(config_obj, w, 400, SparseFileError) + return + } - if r.Method == "HEAD" { - returnError(w, 200, "Ok") - return - } + reader_at = &utils.RangedReader{ + ReaderAt: reader_at, + Index: index, + } + + total_size = calculateTotalSizeWithPadding(index) + } else { + total_size = calculateTotalReaderSize(file) + } + + if request.TextFilter { + output, next_offset, err := filterData(reader_at, request) + if err != nil { + returnError(config_obj, w, 500, err) + return + } - var reader_at io.ReaderAt = &utils.ReaderAtter{Reader: file} + w.Header().Set("Content-Disposition", "attachment; "+ + sanitizeFilenameForAttachment(filename)) + w.Header().Set("Content-Type", + utils.GetMimeString(output, utils.AutoDetectMime(request.DetectMime))) + w.Header().Set("Content-Range", + fmt.Sprintf("bytes %d-%d/%d", request.Offset, next_offset, total_size)) + w.WriteHeader(200) - index, err := getIndex(config_obj, path_spec) + _, _ = w.Write(output) + return + } - // If the file is sparse, we use the sparse reader. - if err == nil && len(index.Ranges) > 0 { - reader_at = &utils.RangedReader{ - ReaderAt: reader_at, - Index: index, + // If the user requested the whole file, and also has password + // set we send them a zip file with the entire thing + if request.ZipFile { + err = streamZipFile(r.Context(), org_config_obj, w, file, filename) + if err == nil { + return + } } - } - offset := request.Offset + emitContentLength(w, int(request.Offset), int(request.Length), total_size) - // From here on we sent the headers and we can not - // really report an error to the client. - filename := strings.Replace(path_spec.Base(), "\"", "_", -1) - w.Header().Set("Content-Disposition", "attachment; filename="+ - url.PathEscape(filename)) - w.Header().Set("Content-Type", "binary/octet-stream") - w.WriteHeader(200) + offset := request.Offset - length_sent := 0 - buf := pool.Get().([]byte) - defer pool.Put(buf) + // Read the first buffer now so we can report errors + length_sent := 0 + headers_sent := false - for { - n, _ := reader_at.ReadAt(buf, offset) - if n > 0 { + // Only allow limited size buffers to be requested by the user. + var buf []byte + if request.Length == 0 || request.Length >= BUFSIZE { + buf = pool.Get().([]byte) + defer pool.Put(buf) + + } else { + buf = make([]byte, request.Length) + } + + for { + n, err := reader_at.ReadAt(buf, offset) + if err != nil && err != io.EOF { + // Only send errors if the headers have not yet been + // sent. + if !headers_sent { + returnError(config_obj, w, 500, err) + } + return + } if request.Length != 0 { length_to_send := request.Length - length_sent if n > length_to_send { n = length_to_send } } - if n == 0 { + if n <= 0 { return } - _, err := w.Write(buf[:n]) + // Write an ok status which includes the attachment name + // but only if no other data was sent. + if !headers_sent { + w.Header().Set("Content-Disposition", "attachment; "+ + sanitizeFilenameForAttachment(filename)) + w.Header().Set("Content-Type", + utils.GetMimeString(buf[:n], + utils.AutoDetectMime(request.DetectMime))) + w.WriteHeader(200) + headers_sent = true + } + + written, err := w.Write(buf[:n]) if err != nil { return } - length_sent += n + + length_sent += written offset += int64(n) - } else { - return + } + }) +} + +// Read data from offset and filter it until the requested number of +// lines is found. This produces text only output, aka "strings" +func filterData(reader_at io.ReaderAt, + request vfsFileDownloadRequest) ( + output []byte, next_offset int64, err error) { + + lines := 0 + required_lines := request.Lines + if required_lines == 0 { + required_lines = 25 + } + offset := request.Offset + + buf := pool.Get().([]byte) + defer pool.Put(buf) + + // This is a safety mechanism in case the file is mostly 0 + total_read := 0 + + for { + if total_read > 10*1024*1024 { + break + } + + n, err := reader_at.ReadAt(buf, offset) + if err != nil && err != io.EOF { + return nil, 0, err + } + + if n <= 0 { + break + } + + total_read += n + + // Read the buffer and filter it collecting only printable + // chars. + for i := 0; i < n; i++ { + c := buf[i] + switch c { + case 0: + continue + + case '\n': + lines++ + if required_lines <= lines { + return output, offset + int64(i), nil + } + fallthrough + + default: + if c >= 0x20 && c < 0x7f || + c == 10 || c == 13 || c == 9 { + output = append(output, c) + } else { + output = append(output, '.') + } } } - }) + offset += int64(n) + } + + return output, offset, nil } func getRows( ctx context.Context, config_obj *config_proto.Config, - request *api_proto.GetTableRequest) ( + request *api_proto.GetTableRequest, + principal string) ( rows <-chan *ordereddict.Dict, close func(), log_path api.FSPathSpec, err error) { file_store_factory := file_store.GetFileStore(config_obj) // We want an event table. if request.Type == "CLIENT_EVENT" || request.Type == "SERVER_EVENT" { - path_manager, err := artifacts.NewArtifactPathManager( + path_manager, err := artifacts.NewArtifactPathManager(ctx, config_obj, request.ClientId, request.FlowId, request.Artifact) if err != nil { @@ -219,19 +430,45 @@ func getRows( } rs_reader, err := result_sets.NewTimedResultSetReader( - ctx, file_store_factory, path_manager) + ctx, config_obj, path_manager) + + return rs_reader.Rows(ctx), rs_reader.Close, log_path, err + + } else if request.Type == "STACK" { + log_path = path_specs.NewUnsafeFilestorePath( + utils.FilterSlice(request.StackPath, "")...). + SetType(api.PATH_TYPE_FILESTORE_JSON) + + options, err := tables.GetTableOptions(request) + if err != nil { + return nil, nil, nil, err + } + + rs_reader, err := result_sets.NewResultSetReaderWithOptions( + ctx, config_obj, file_store_factory, log_path, options) + if err != nil { + return nil, nil, nil, err + } return rs_reader.Rows(ctx), rs_reader.Close, log_path, err } else { - log_path, err := getPathSpec(config_obj, request) + log_path, err := tables.GetPathSpec( + ctx, config_obj, request, principal) if err != nil { return nil, nil, nil, err } - rs_reader, err := result_sets.NewResultSetReader( - file_store_factory, log_path) + options, err := tables.GetTableOptions(request) + if err != nil { + return nil, nil, nil, err + } + rs_reader, err := result_sets.NewResultSetReaderWithOptions( + ctx, config_obj, file_store_factory, log_path, options) + if err != nil { + return nil, nil, nil, err + } return rs_reader.Rows(ctx), rs_reader.Close, log_path, err } } @@ -240,6 +477,7 @@ func getRows( // exporting we need to replicate this transformation, otherwise the // results can be surprising. func getTransformer( + ctx context.Context, config_obj *config_proto.Config, in *api_proto.GetTableRequest) func(row *ordereddict.Dict) *ordereddict.Dict { if in.HuntId != "" && in.Type == "clients" { @@ -247,16 +485,24 @@ func getTransformer( client_id := utils.GetString(row, "ClientId") flow_id := utils.GetString(row, "FlowId") - flow, err := flows.LoadCollectionContext(config_obj, client_id, flow_id) + base := ordereddict.NewDict(). + Set("ClientId", client_id). + Set("Hostname", services.GetHostname(ctx, config_obj, client_id)). + Set("FlowId", flow_id). + Set("StartedTime", time.Unix(utils.GetInt64(row, "Timestamp"), 0)) + + launcher, err := services.GetLauncher(config_obj) if err != nil { - flow = flows.NewCollectionContext(config_obj) + return base.Set("State", fmt.Sprintf("Unknown: %v", err)) } - return ordereddict.NewDict(). - Set("ClientId", client_id). - Set("Hostname", services.GetHostname(client_id)). - Set("FlowId", flow_id). - Set("StartedTime", time.Unix(utils.GetInt64(row, "Timestamp"), 0)). + flow, err := launcher.Storage().LoadCollectionContext( + ctx, config_obj, client_id, flow_id) + if err != nil { + return base.Set("State", fmt.Sprintf("Unknown: %v", err)) + } + + return base. Set("State", flow.State.String()). Set("Duration", flow.ExecutionDuration/1000000000). Set("TotalBytes", flow.TotalUploadedBytes). @@ -268,116 +514,275 @@ func getTransformer( return func(row *ordereddict.Dict) *ordereddict.Dict { return row } } -// Download the table as specified by the v1/GetTable API. -func downloadTable(config_obj *config_proto.Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - request := &api_proto.GetTableRequest{} - decoder := schema.NewDecoder() - decoder.SetAliasTag("json") - err := decoder.Decode(request, r.URL.Query()) - if err != nil { - returnError(w, 404, err.Error()) - return - } +func downloadFileStore( + config_obj *config_proto.Config, prefix []string) http.Handler { + return api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + components := utils.SplitComponents(r.URL.Path) - row_chan, closer, log_path, err := getRows( - r.Context(), config_obj, request) - if err != nil { - returnError(w, 400, "Invalid request") - return - } - defer closer() + // make sure the prefix is correct + for i, p := range prefix { + if len(components) <= i || p != components[i] { + returnError(config_obj, w, 404, utils.NotFoundError) + return + } + } - transform := getTransformer(config_obj, request) + path_spec := path_specs.FromGenericComponentList(components) - download_name := request.DownloadFilename - if download_name == "" { - download_name = strings.Replace(log_path.Base(), "\"", "", -1) - } + org_id := authenticators.GetOrgIdFromRequest(r) + org_manager, err := services.GetOrgManager() + if err != nil { + returnError(config_obj, w, 404, err) + return + } + + org_config_obj, err := org_manager.GetOrgConfig(org_id) + if err != nil { + returnError(config_obj, w, 404, err) + return + } - // Log an audit event. - userinfo := GetUserInfo(r.Context(), config_obj) + // The following is not strictly necessary because this + // function is behind the authenticator middleware which means + // that if we get here the user is already authenticated and + // has at least read permissions on this org. But we check + // again to make sure we are resilient against possible + // regressions in the authenticator code. + users := services.GetUserManager() + user_record, err := users.GetUserFromHTTPContext(r.Context()) + if err != nil { + returnError(config_obj, w, 404, err) + return + } - // This should never happen! - if userinfo.Name == "" { - returnError(w, 500, "Unauthenticated access.") - return - } + principal := user_record.Name + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + returnError(config_obj, w, 403, errors.New("User is not allowed to read files.")) + return + } + + err = file_store_accessor.IsFileAccessible(path_spec) + if err != nil { + returnError(config_obj, w, 404, err) + return + } + + file_store_factory := file_store.GetFileStore(org_config_obj) + fd, err := file_store_factory.ReadFile(path_spec) + if err != nil { + returnError(config_obj, w, 404, err) + return + } + + buf := pool.Get().([]byte) + defer pool.Put(buf) - switch request.DownloadFormat { - case "csv": - download_name = strings.TrimSuffix(download_name, ".json") - download_name += ".csv" + // Read the first buffer for mime detection. + n, err := fd.Read(buf) + if err != nil { + returnError(config_obj, w, 404, err) + return + } // From here on we already sent the headers and we can // not really report an error to the client. - w.Header().Set("Content-Disposition", "attachment; filename="+ - url.PathEscape(download_name)) - w.Header().Set("Content-Type", "binary/octet-stream") + w.Header().Set("Content-Disposition", "attachment; "+ + sanitizePathspecForAttachment(path_spec)) + + w.Header().Set("Content-Type", + utils.GetMimeString(buf[:n], utils.AutoDetectMime(true))) w.WriteHeader(200) + _, _ = w.Write(buf[:n]) + + // Copy the rest directly. + _, _ = utils.Copy(r.Context(), w, fd) + }) +} - logger := logging.GetLogger(config_obj, &logging.Audit) - logger.WithFields(logrus.Fields{ - "user": userinfo.Name, - "request": request, - "remote": r.RemoteAddr, - }).Info("DownloadTable") +// Allowed chars in non extended names +const allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$&+-.^_`|~@'=()[]{}0123456789 " - scope := vql_subsystem.MakeScope() - csv_writer := csv.GetCSVAppender(scope, w, true /* write_headers */) - for row := range row_chan { - csv_writer.Write( - filterColumns(request.Columns, transform(row))) +func sanitizePathspecForAttachment(path_spec api.FSPathSpec) string { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition + // > The string following filename should always be put into quotes; + base_filename := path_spec.Base() + api.GetExtensionForFilestore(path_spec) + return sanitizeFilenameForAttachment(base_filename) +} + +func sanitizeFilenameForAttachment(base_filename string) string { + // If the base filename contains path separator we use the last one + if strings.Contains(base_filename, "/") { + parts := strings.Split(base_filename, "/") + base_filename = parts[len(parts)-1] + } + + base_filename_ascii := []byte{} + for _, c := range base_filename { + if strings.Contains(allowedChars, string(c)) { + base_filename_ascii = append(base_filename_ascii, byte(c)) + } else { + base_filename_ascii = append(base_filename_ascii, '_') + } + } + + // The `filename*` parameter has to be encoded accroding to + // RFC5987 without leading and trailing quotes or this fails in + // Firefox. + return fmt.Sprintf("filename*=utf-8''%s; filename=\"%s\" ", + url.PathEscape(base_filename), url.PathEscape(string(base_filename_ascii))) +} + +// Download the table as specified by the v1/GetTable API. +func downloadTable(config_obj *config_proto.Config) http.Handler { + return api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + request := &api_proto.GetTableRequest{} + decoder := schema.NewDecoder() + decoder.IgnoreUnknownKeys(true) + + decoder.SetAliasTag("json") + err := decoder.Decode(request, r.URL.Query()) + if err != nil { + returnError(config_obj, w, 404, err) + return + } + + org_manager, err := services.GetOrgManager() + if err != nil { + returnError(config_obj, w, 404, err) + return } - csv_writer.Close() - // Output in jsonl by default. - default: - if !strings.HasSuffix(download_name, ".json") { - download_name += ".json" + org_config_obj, err := org_manager.GetOrgConfig(request.OrgId) + if err != nil { + returnError(config_obj, w, 404, err) + return } - // From here on we already sent the headers and we can - // not really report an error to the client. - w.Header().Set("Content-Disposition", "attachment; filename="+ - url.PathEscape(download_name)) - w.Header().Set("Content-Type", "binary/octet-stream") - w.WriteHeader(200) + user_record := GetUserInfo(r.Context(), org_config_obj) + principal := user_record.Name + + // This should never happen! + if principal == "" { + returnError(config_obj, w, 403, UnauthenticatedAccessError) + return + } + + row_chan, closer, log_path, err := getRows( + r.Context(), org_config_obj, request, principal) + if err != nil { + returnError(config_obj, w, 400, InvalidRequestError) + return + } + defer closer() - logger := logging.GetLogger(config_obj, &logging.Audit) - logger.WithFields(logrus.Fields{ - "user": userinfo.Name, - "request": request, - "remote": r.RemoteAddr, - }).Info("DownloadTable") + transform := getTransformer(r.Context(), org_config_obj, request) + + download_name := request.DownloadFilename + if download_name == "" { + download_name = strings.Replace(log_path.Base(), "\"", "", -1) + } - for row := range row_chan { - serialized, err := json.Marshal( - filterColumns(request.Columns, transform(row))) + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + returnError(config_obj, w, 403, UnauthenticatedAccessError) + return + } + + opts := json.GetJsonOptsForTimezone(request.Timezone) + switch request.DownloadFormat { + case "csv": + download_name = strings.TrimSuffix(download_name, ".json") + download_name += ".csv" + + // From here on we already sent the headers and we can + // not really report an error to the client. + w.Header().Set("Content-Disposition", "attachment; "+ + sanitizeFilenameForAttachment(download_name)) + w.Header().Set("Content-Type", "binary/octet-stream") + w.WriteHeader(200) + + err := services.LogAudit(r.Context(), + org_config_obj, principal, "DownloadTable", + ordereddict.NewDict(). + Set("request", request). + Set("remote", r.RemoteAddr)) if err != nil { - return + logger := logging.GetLogger( + org_config_obj, &logging.FrontendComponent) + logger.Error("DownloadTable %v %v", + principal, request) + } + + scope := vql_subsystem.MakeScope() + csv_writer := csv.GetCSVAppender( + org_config_obj, scope, w, + csv.WriteHeaders, opts) + for row := range row_chan { + csv_writer.Write( + filterColumns(request.Columns, transform(row))) + } + csv_writer.Close() + + // Output in jsonl by default. + default: + if !strings.HasSuffix(download_name, ".json") { + download_name += ".json" } - // Write line delimited JSON - _, _ = w.Write(serialized) - _, _ = w.Write([]byte{'\n'}) + // From here on we already sent the headers and we can + // not really report an error to the client. + w.Header().Set("Content-Disposition", "attachment; "+ + sanitizeFilenameForAttachment(download_name)) + w.Header().Set("Content-Type", "binary/octet-stream") + w.WriteHeader(200) + + err = services.LogAudit(r.Context(), + org_config_obj, principal, "DownloadTable", + ordereddict.NewDict(). + Set("request", request). + Set("remote", r.RemoteAddr)) + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.FrontendComponent) + logger.Error("DownloadTable %v %v", principal, request) + } + + for row := range row_chan { + serialized, err := json.MarshalWithOptions( + filterColumns(request.Columns, transform(row)), + json.GetJsonOptsForTimezone(request.Timezone)) + if err != nil { + return + } + + // Write line delimited JSON + _, _ = w.Write(serialized) + _, _ = w.Write([]byte{'\n'}) + } } - } - }) + }) } -func vfsGetBuffer( - config_obj *config_proto.Config, - client_id string, vfs_path api.FSPathSpec, offset uint64, length uint32) ( +func vfsGetBuffer(config_obj *config_proto.Config, client_id string, + vfs_path api.FSPathSpec, offset uint64, length uint32, padding bool) ( *api_proto.VFSFileBuffer, error) { + err := file_store_accessor.IsFileAccessible(vfs_path) + if err != nil { + return nil, err + } + file, err := file_store.GetFileStore(config_obj).ReadFile(vfs_path) if err != nil { return nil, err } defer file.Close() - var reader_at io.ReaderAt = &utils.ReaderAtter{Reader: file} + var reader_at io.ReaderAt = utils.MakeReaderAtter(file) result := &api_proto.VFSFileBuffer{ Data: make([]byte, length), @@ -387,7 +792,7 @@ func vfsGetBuffer( index, err := getIndex(config_obj, vfs_path) // If the file is sparse, we use the sparse reader. - if err == nil && len(index.Ranges) > 0 { + if err == nil && padding && len(index.Ranges) > 0 { reader_at = &utils.RangedReader{ ReaderAt: reader_at, Index: index, @@ -395,8 +800,9 @@ func vfsGetBuffer( } n, err := reader_at.ReadAt(result.Data, int64(offset)) - if err != nil && errors.Is(err, os.ErrNotExist) && - errors.Cause(err) != io.ErrUnexpectedEOF { + if err != nil && + errors.Is(err, os.ErrNotExist) && + !errors.Is(err, io.ErrUnexpectedEOF) { return nil, err } @@ -409,6 +815,11 @@ func getIndex(config_obj *config_proto.Config, vfs_path api.FSPathSpec) (*actions_proto.Index, error) { index := &actions_proto.Index{} + err := file_store_accessor.IsFileAccessible(vfs_path) + if err != nil { + return nil, err + } + file_store_factory := file_store.GetFileStore(config_obj) fd, err := file_store_factory.ReadFile( vfs_path.SetType(api.PATH_TYPE_FILESTORE_SPARSE_IDX)) @@ -417,7 +828,7 @@ func getIndex(config_obj *config_proto.Config, } defer fd.Close() - data, err := ioutil.ReadAll(fd) + data, err := utils.ReadAllWithLimit(fd, constants.MAX_MEMORY) if err != nil { return nil, err } @@ -442,3 +853,102 @@ func filterColumns(columns []string, row *ordereddict.Dict) *ordereddict.Dict { } return new_row } + +func calculateTotalSizeWithPadding(index *actions_proto.Index) int { + size := 0 + for _, r := range index.Ranges { + size += int(r.Length) + } + return size +} + +func calculateTotalReaderSize(reader api.FileReader) int { + stat, err := reader.Stat() + if err == nil { + return int(stat.Size()) + } + return 0 +} + +func emitContentLength(w http.ResponseWriter, offset int, req_length int, size int) { + // Size is not known or 0, do not send a Content Length + if size == 0 || offset > size { + return + } + + // How much data is available to read in the file. + available := size - offset + + // If the user asked for less data than is available, then we will + // return less, otherwise we only return how much data is + // available. + if req_length > BUFSIZE { + req_length = BUFSIZE + } + + // req_length of 0 means download the entire file without byte ranges. + if req_length > 0 && req_length < available { + available = req_length + } + + w.Header().Set("Content-Length", fmt.Sprintf("%v", available)) +} + +func streamZipFile( + ctx context.Context, + config_obj *config_proto.Config, + w http.ResponseWriter, + file io.Reader, filename string) error { + buf := pool.Get().([]byte) + defer pool.Put(buf) + + w.Header().Set("Content-Disposition", "attachment; "+ + sanitizeFilenameForAttachment(filename+".zip")) + w.Header().Set("Content-Type", "application/zip") + w.WriteHeader(200) + + users := services.GetUserManager() + user_record, err := users.GetUserFromHTTPContext(ctx) + if err != nil { + return err + } + + // Get the user's preferences to set the container password + password := "" + options, err := users.GetUserOptions(ctx, user_record.Name) + if err == nil { + password = options.DefaultPassword + } + + container, err := reporting.NewContainerFromWriter( + fmt.Sprintf("HTTPDownload-%v", filename), + config_obj, utils.NopWriteCloser{Writer: w}, password, 5, nil) + if err != nil { + return err + } + defer container.Close() + + file_writer, err := container.Create(filename, utils.Now()) + if err != nil { + return err + } + defer file_writer.Close() + + for { + n, err := file.Read(buf) + if n == 0 || err == io.EOF { + break + } + + if err != nil { + return err + } + + _, err = file_writer.Write(buf[:n]) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/events.go b/api/events.go index a32e432b5..052069216 100644 --- a/api/events.go +++ b/api/events.go @@ -1,28 +1,17 @@ package api import ( - "crypto/x509" - "os" - "sort" - "strings" + "context" - context "golang.org/x/net/context" + errors "github.com/go-errors/errors" "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/peer" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" "www.velocidex.com/golang/velociraptor/acls" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" api_proto "www.velocidex.com/golang/velociraptor/api/proto" - config_proto "www.velocidex.com/golang/velociraptor/config/proto" - crypto_utils "www.velocidex.com/golang/velociraptor/crypto/utils" - file_store "www.velocidex.com/golang/velociraptor/file_store" - "www.velocidex.com/golang/velociraptor/file_store/api" - "www.velocidex.com/golang/velociraptor/paths/artifacts" - "www.velocidex.com/golang/velociraptor/result_sets" + "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/services" - users "www.velocidex.com/golang/velociraptor/users" "www.velocidex.com/golang/velociraptor/utils" ) @@ -30,281 +19,123 @@ func (self *ApiServer) PushEvents( ctx context.Context, in *api_proto.PushEventRequest) (*emptypb.Empty, error) { - // Get the TLS context from the peer and verify its - // certificate. - peer, ok := peer.FromContext(ctx) - if !ok { - return nil, status.Error(codes.InvalidArgument, "cant get peer info") - } + defer Instrument("PushEvents")() - tlsInfo, ok := peer.AuthInfo.(credentials.TLSInfo) - if !ok { - return nil, status.Error(codes.InvalidArgument, "unable to get credentials") + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) } - // Authenticate API clients using certificates. - for _, peer_cert := range tlsInfo.State.PeerCertificates { - chains, err := peer_cert.Verify( - x509.VerifyOptions{Roots: self.ca_pool}) + // User is asking to switch orgs + if in.OrgId != "" { + org_manager, err := services.GetOrgManager() if err != nil { - return nil, err - } - - if len(chains) == 0 { - return nil, status.Error(codes.InvalidArgument, "no chains verified") - } - - peer_name := crypto_utils.GetSubjectName(peer_cert) - if peer_name != self.config.Client.PinnedServerName { - token, err := acls.GetEffectivePolicy(self.config, peer_name) - if err != nil { - return nil, err - } - - // Check that the principal is allowed to push to the queue. - ok, err := acls.CheckAccessWithToken(token, acls.PUBLISH, in.Artifact) - if err != nil { - return nil, err - } - - if !ok { - return nil, status.Error(codes.PermissionDenied, - "Permission denied: PUBLISH "+peer_name+" to "+in.Artifact) - } + return nil, Status(self.verbose, err) } - rows, err := utils.ParseJsonToDicts([]byte(in.Jsonl)) + org_config_obj, err = org_manager.GetOrgConfig(in.OrgId) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - - // Only return the first row - journal, err := services.GetJournal() - if err != nil { - return nil, err - } - - // only broadcast the events for local listeners. Minions - // write the events themselves, so we just need to broadcast - // for any server event artifacts that occur. - journal.Broadcast(self.config, - rows, in.Artifact, in.ClientId, in.FlowId) - return &emptypb.Empty{}, err } - return nil, status.Error(codes.InvalidArgument, "no peer certs?") -} - -func (self *ApiServer) WriteEvent( - ctx context.Context, - in *actions_proto.VQLResponse) (*emptypb.Empty, error) { - - // Get the TLS context from the peer and verify its - // certificate. - peer, ok := peer.FromContext(ctx) - if !ok { - return nil, status.Error(codes.InvalidArgument, "cant get peer info") - } + user_name := user_record.Name - tlsInfo, ok := peer.AuthInfo.(credentials.TLSInfo) - if !ok { - return nil, status.Error(codes.InvalidArgument, "unable to get credentials") - } - - // Authenticate API clients using certificates. - for _, peer_cert := range tlsInfo.State.PeerCertificates { - chains, err := peer_cert.Verify( - x509.VerifyOptions{Roots: self.ca_pool}) + // Now check permmissions in the org if the user is not the superuser. + if user_name != utils.GetSuperuserName(org_config_obj) { + token, err := services.GetEffectivePolicy(org_config_obj, user_name) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - if len(chains) == 0 { - return nil, status.Error(codes.InvalidArgument, "no chains verified") - } - - peer_name := crypto_utils.GetSubjectName(peer_cert) - - token, err := acls.GetEffectivePolicy(self.config, peer_name) + // Check that the principal is allowed to push to this specific queue. + ok, err := services.CheckAccessWithToken(token, acls.PUBLISH, in.Artifact) if err != nil { - return nil, err - } - - // Check that the principal is allowed to push to the queue. - ok, err := acls.CheckAccessWithToken(token, - acls.MACHINE_STATE, in.Query.Name) - if err != nil { - return nil, err + return nil, Status(self.verbose, err) } if !ok { return nil, status.Error(codes.PermissionDenied, - "Permission denied: MACHINE_STATE "+ - peer_name+" to "+in.Query.Name) - } - - rows, err := utils.ParseJsonToDicts([]byte(in.Response)) - if err != nil { - return nil, err + "Permission denied: PUBLISH "+user_name+" to "+in.Artifact) } - // Only return the first row - if true { - journal, err := services.GetJournal() - if err != nil { - return nil, err - } - - err = journal.PushRowsToArtifact(self.config, - rows, in.Query.Name, peer_name, "") - return &emptypb.Empty{}, err - } - } - - return nil, status.Error(codes.InvalidArgument, "no peer certs?") -} - -func (self *ApiServer) ListAvailableEventResults( - ctx context.Context, - in *api_proto.ListAvailableEventResultsRequest) ( - *api_proto.ListAvailableEventResultsResponse, error) { + // For regular users append the sender field so we can track + // where the message came from. + in.Jsonl = json.AppendJsonlItem(in.Jsonl, "_Sender", user_name) + in.Username = user_name - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) - if err != nil { - return nil, err + // Always write user events. + in.Write = true } - permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) - if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, - "User is not allowed to view results.") + if in.Username == "" { + in.Username = user_name } - if in.Artifact == "" { - return listAvailableEventArtifacts(self, in) + rows, err := utils.ParseJsonToDicts([]byte(in.Jsonl)) + if err != nil { + return nil, Status(self.verbose, err) } - return listAvailableEventTimestamps(ctx, self, in) -} -func listAvailableEventTimestamps( - ctx context.Context, - self *ApiServer, in *api_proto.ListAvailableEventResultsRequest) ( - *api_proto.ListAvailableEventResultsResponse, error) { - - path_manager, err := artifacts.NewArtifactPathManager( - self.config, in.ClientId, "", in.Artifact) + // Only return the first row + journal, err := services.GetJournal(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - result := &api_proto.ListAvailableEventResultsResponse{ - Logs: []*api_proto.AvailableEvent{ - { - Artifact: in.Artifact, - }, - }, + journal_opts := services.JournalOptions{ + ArtifactName: in.Artifact, + ClientId: in.ClientId, + FlowId: in.FlowId, + Username: in.Username, } - timestamps, err := listAvailableEventTimestampFiles(ctx, self, path_manager) - result.Logs[0].RowTimestamps = timestamps + // only broadcast the events for local listeners. Minions + // write the events themselves, so we just need to broadcast + // for any server event artifacts that occur. + if in.Write { + err = journal.PushRowsToArtifact(ctx, org_config_obj, + rows, journal_opts) - timestamps, err = listAvailableEventTimestampFiles( - ctx, self, path_manager.Logs()) - result.Logs[0].LogTimestamps = timestamps + } else { + err = journal.Broadcast(ctx, org_config_obj, rows, journal_opts) + } - return result, nil + return &emptypb.Empty{}, err } -func listAvailableEventTimestampFiles( - ctx context.Context, self *ApiServer, path_manager api.PathManager) ([]int32, error) { - result := []int32{} - - file_store_factory := file_store.GetFileStore(self.config) - reader, err := result_sets.NewTimedResultSetReader( - ctx, file_store_factory, path_manager) - if err != nil { - return nil, err - } - - for _, prop := range reader.GetAvailableFiles(ctx) { - result = append(result, int32(prop.StartTime.Unix())) - } - return result, nil +func (self *ApiServer) WriteEvent( + ctx context.Context, + in *actions_proto.VQLResponse) (*emptypb.Empty, error) { + return nil, Status(self.verbose, + errors.New("WriteEvent is deprecated, please use PushEvents instead")) } -func listAvailableEventArtifacts( - self *ApiServer, in *api_proto.ListAvailableEventResultsRequest) ( +func (self *ApiServer) ListAvailableEventResults( + ctx context.Context, + in *api_proto.ListAvailableEventResultsRequest) ( *api_proto.ListAvailableEventResultsResponse, error) { - // Figure out where all the monitoring artifacts logs are - // stored by looking at some examples. - exemplar := "Generic.Client.Stats" - if in.ClientId == "" || in.ClientId == "server" { - exemplar = "Server.Monitor.Health" - } + defer Instrument("ListAvailableEventResults")() - path_manager, err := artifacts.NewArtifactPathManager( - self.config, in.ClientId, "", exemplar) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - // getAllArtifacts analyses the path name from disk and adds - // to the events list. - seen := make(map[string]*api_proto.AvailableEvent) - err = getAllArtifacts(self.config, path_manager.GetRootPath(), seen) - if err != nil { - return nil, err + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, user_record.Name, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to view results.") } - err = getAllArtifacts(self.config, path_manager.Logs().GetRootPath(), seen) + client_monitoring_service, err := services.ClientEventManager(org_config_obj) if err != nil { - return nil, err - } - - result := &api_proto.ListAvailableEventResultsResponse{} - for _, item := range seen { - result.Logs = append(result.Logs, item) + return nil, Status(self.verbose, err) } - sort.Slice(result.Logs, func(i, j int) bool { - return result.Logs[i].Artifact < result.Logs[j].Artifact - }) - - return result, nil -} - -func getAllArtifacts( - config_obj *config_proto.Config, - log_path api.FSPathSpec, - seen map[string]*api_proto.AvailableEvent) error { - - file_store_factory := file_store.GetFileStore(config_obj) - - return api.Walk(file_store_factory, log_path, - func(full_path api.FSPathSpec, info os.FileInfo) error { - // Walking the events directory will give us - // all the day json files. Each day json file - // is contained in a directory structure which - // reflects the name of the artifact, for - // example: - - // /Server.Monitor.Health/Prometheus/2021-08-01.json - // Corresponds to the artifact Server.Monitor.Health/Prometheus - if !info.IsDir() && info.Size() > 0 { - relative_path := full_path.Dir(). - Components()[len(log_path.Components()):] - artifact_name := strings.Join(relative_path, "/") - event, pres := seen[artifact_name] - if !pres { - event = &api_proto.AvailableEvent{ - Artifact: artifact_name, - } - seen[artifact_name] = event - } - } - return nil - }) + return client_monitoring_service.ListAvailableEventResults(ctx, in) } diff --git a/api/events_test.go b/api/events_test.go new file mode 100644 index 000000000..b25b5e8c8 --- /dev/null +++ b/api/events_test.go @@ -0,0 +1,432 @@ +package api_test + +import ( + "io" + "strings" + "testing" + "time" + + "github.com/Velocidex/ordereddict" + errors "github.com/go-errors/errors" + "github.com/stretchr/testify/suite" + acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" + actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" + "www.velocidex.com/golang/velociraptor/api" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/crypto" + "www.velocidex.com/golang/velociraptor/file_store" + file_store_api "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/file_store/path_specs" + "www.velocidex.com/golang/velociraptor/file_store/test_utils" + "www.velocidex.com/golang/velociraptor/grpc_client" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/paths/artifact_modes" + "www.velocidex.com/golang/velociraptor/paths/artifacts" + "www.velocidex.com/golang/velociraptor/result_sets" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/vtesting" + "www.velocidex.com/golang/velociraptor/vtesting/assert" +) + +var ( + definitions = []string{` +name: Server.Audit.Logs +type: SERVER_EVENT +`} + + FALSE = false +) + +// Tests the public API endpoints +type GeneralAPITest struct { + test_utils.TestSuite + + client_config *config_proto.Config + username string +} + +func (self *GeneralAPITest) SetupTest() { + self.ConfigObj = self.LoadConfig() + self.ConfigObj.API.BindPort = 8787 + + self.LoadArtifactsIntoConfig(definitions) + + self.TestSuite.SetupTest() + + // Generate an API client. + self.username = "TestApiUser" + bundle, err := crypto.GenerateServerCert( + self.ConfigObj, self.username) + assert.NoError(self.T(), err) + + // Reset the connection pool for each test. + grpc_client.Factory = &grpc_client.DummyGRPCAPIClient{} + + self.ConfigObj.ApiConfig = &config_proto.ApiClientConfig{ + CaCertificate: self.ConfigObj.Client.CaCertificate, + ClientCert: bundle.Cert, + ClientPrivateKey: string(bundle.PrivateKey), + Name: self.username, + } + + server_builder, err := api.NewServerBuilder( + self.Sm.Ctx, self.ConfigObj, self.Sm.Wg) + assert.NoError(self.T(), err) + + err = server_builder.WithAPIServer(self.Sm.Ctx, self.Sm.Wg) + assert.NoError(self.T(), err) + + // Now bring up an API server. + self.ConfigObj.Services = &config_proto.ServerServicesConfig{} + + // Wait for the server to come up. + vtesting.WaitUntil(2*time.Second, self.T(), func() bool { + conn, closer, err := grpc_client.Factory.GetAPIClient( + self.Sm.Ctx, grpc_client.API_User, self.ConfigObj) + assert.NoError(self.T(), err) + defer closer() + + res, err := conn.Check(self.Sm.Ctx, &api_proto.HealthCheckRequest{}) + return err == nil && res.Status == api_proto.HealthCheckResponse_SERVING + }) +} + +func (self *GeneralAPITest) TestQuery() { + // Create the user + user_manager := services.GetUserManager() + err := user_manager.SetUser(self.Ctx, &api_proto.VelociraptorUser{ + Name: self.username, + }) + + // Make the user a reader on the root org. + err = services.GrantUserToOrg(self.Ctx, + utils.GetSuperuserName(self.ConfigObj), + self.username, + []string{"root"}, &acl_proto.ApiClientACL{ + Roles: []string{"reader"}, + // User is not permitted to access over the API. + }) + assert.NoError(self.T(), err) + + message := &actions_proto.VQLCollectorArgs{ + Query: []*actions_proto.VQLRequest{ + {VQL: "SELECT * FROM info()"}, + }, + } + + resp, err := self.getQueryResults(message) + assert.Error(self.T(), err) + assert.Contains(self.T(), err.Error(), + "Permission denied: User TestApiUser requires permission ANY_QUERY") + assert.Equal(self.T(), len(resp), 0) + + // Now give the user the any query permission. + err = services.GrantUserToOrg(self.Ctx, + utils.GetSuperuserName(self.ConfigObj), + self.username, + []string{"root"}, &acl_proto.ApiClientACL{ + Roles: []string{"reader"}, + AnyQuery: true, + }) + assert.NoError(self.T(), err) + + // Try again: Query is allowed to run now but it is running with + // reduced permissions. The VQL engine itself will enforce + // permissions. + resp, err = self.getQueryResults(message) + assert.NoError(self.T(), err) + assert.True(self.T(), self.containsLog( + resp, "PermissionDenied: Permission denied: [MACHINE_STATE]"), + json.MustMarshalString(resp)) + + // Now give the user also the MACHINE_STATE permission. + err = services.GrantUserToOrg(self.Ctx, + utils.GetSuperuserName(self.ConfigObj), + self.username, + []string{"root"}, &acl_proto.ApiClientACL{ + Roles: []string{"reader"}, + AnyQuery: true, + MachineState: true, + }) + assert.NoError(self.T(), err) + + // It works fine now. + resp, err = self.getQueryResults(message) + assert.NoError(self.T(), err) + assert.True(self.T(), !self.containsLog(resp, "PermissionDenied"), + json.MustMarshalString(resp)) +} + +func (self *GeneralAPITest) containsLog(resp []*actions_proto.VQLResponse, log string) bool { + for _, r := range resp { + if strings.Contains(r.Log, log) { + return true + } + } + return false +} + +func (self *GeneralAPITest) getQueryResults( + message *actions_proto.VQLCollectorArgs) ( + res []*actions_proto.VQLResponse, err error) { + + client, closer, err := grpc_client.Factory.GetAPIClient( + self.Ctx, grpc_client.API_User, self.ConfigObj) + assert.NoError(self.T(), err) + + defer closer() + + receiver, err := client.Query(self.Ctx, message) + if err != nil { + return nil, err + } + + for { + response, err := receiver.Recv() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return res, err + } + res = append(res, response) + } + + return res, nil +} + +func (self *GeneralAPITest) TestVFSGetBuffer() { + filename := path_specs.NewSafeFilestorePath("A", "B", "C"). + SetType(file_store_api.PATH_TYPE_FILESTORE_ANY) + + // Write some data to the filestore + file_store_factory := file_store.GetFileStore(self.ConfigObj) + writer, err := file_store_factory.WriteFile(filename) + assert.NoError(self.T(), err) + + _, err = writer.Write([]byte("Hello")) + assert.NoError(self.T(), err) + + writer.Close() + + // Create the user + user_manager := services.GetUserManager() + err = user_manager.SetUser(self.Ctx, &api_proto.VelociraptorUser{ + Name: self.username, + }) + + // Make the user a reader on the root org. + err = services.GrantUserToOrg(self.Ctx, + utils.GetSuperuserName(self.ConfigObj), + self.username, + []string{"root"}, &acl_proto.ApiClientACL{ + // No permissions + Roles: []string{}, + }) + assert.NoError(self.T(), err) + + client, closer, err := grpc_client.Factory.GetAPIClient( + self.Ctx, grpc_client.API_User, self.ConfigObj) + assert.NoError(self.T(), err) + + defer closer() + + message := &api_proto.VFSFileBuffer{ + Components: filename.Components(), + Length: 100, + } + + buf, err := client.VFSGetBuffer(self.Ctx, message) + assert.Error(self.T(), err) + assert.Contains(self.T(), err.Error(), + "PermissionDenied desc = User is not allowed to view the VFS") + + // Give permission and try again. + err = services.GrantUserToOrg(self.Ctx, + utils.GetSuperuserName(self.ConfigObj), + self.username, + []string{"root"}, &acl_proto.ApiClientACL{ + Roles: []string{"reader"}, + }) + assert.NoError(self.T(), err) + + buf, err = client.VFSGetBuffer(self.Ctx, message) + assert.NoError(self.T(), err) + assert.Equal(self.T(), string(buf.Data), "Hello") +} + +func (self *GeneralAPITest) TestVFSGetBufferSparse() { + // Create a sparse file that contains "HelloWorld" + filename := path_specs.FromGenericComponentList([]string{"sparse_upload.txt"}). + SetType(file_store_api.PATH_TYPE_FILESTORE_ANY) + filename_idx := filename.SetType(file_store_api.PATH_TYPE_FILESTORE_SPARSE_IDX) + + file_store_factory := file_store.GetFileStore(self.ConfigObj) + + w, err := file_store_factory.WriteFile(filename) + assert.NoError(self.T(), err) + w.Write([]byte("HelloWorld")) + w.Close() + + // Only 10 bytes are written to the filestore. + stat_file, err := file_store_factory.StatFile(filename) + assert.NoError(self.T(), err) + assert.Equal(self.T(), stat_file.Size(), int64(10)) + + w, err = file_store_factory.WriteFile(filename_idx) + assert.NoError(self.T(), err) + + // Original offset refers to the offset in the remote sparse file. + // file offset refers to the offset within the filestore file + w.Write([]byte(` +{ + "ranges": [ + { + "file_offset": 0, + "original_offset": 0, + "file_length": 5, + "length": 5 + }, + { + "file_offset": 5, + "original_offset": 5, + "length": 5, + "file_length": 0 + }, + { + "file_offset": 5, + "original_offset": 10, + "file_length": 5, + "length": 5 + } + ] +}`)) // This represents: Hello<.....>World with the gap being sparse. + w.Close() + + // Create the user + user_manager := services.GetUserManager() + err = user_manager.SetUser(self.Ctx, &api_proto.VelociraptorUser{ + Name: self.username, + }) + + // Make the user a reader on the root org. + err = services.GrantUserToOrg(self.Ctx, + utils.GetSuperuserName(self.ConfigObj), + self.username, + []string{"root"}, &acl_proto.ApiClientACL{ + Roles: []string{"reader"}, + }) + assert.NoError(self.T(), err) + + client, closer, err := grpc_client.Factory.GetAPIClient( + self.Ctx, grpc_client.API_User, self.ConfigObj) + assert.NoError(self.T(), err) + + defer closer() + + // Read padded buffer this should default to padding = true + buf, err := client.VFSGetBuffer(self.Ctx, &api_proto.VFSFileBuffer{ + Components: filename.Components(), + Length: 100, + }) + assert.NoError(self.T(), err) + assert.Equal(self.T(), string(buf.Data), "Hello\x00\x00\x00\x00\x00World") + + // Read unpadded buffer + buf, err = client.VFSGetBuffer(self.Ctx, &api_proto.VFSFileBuffer{ + Components: filename.Components(), + Length: 100, + Padding: &FALSE, + }) + assert.NoError(self.T(), err) + assert.Equal(self.T(), string(buf.Data), "HelloWorld") +} + +func (self *GeneralAPITest) TestPushEvents() { + client, closer, err := grpc_client.Factory.GetAPIClient( + self.Ctx, grpc_client.API_User, self.ConfigObj) + assert.NoError(self.T(), err) + + defer closer() + + // Create the user + user_manager := services.GetUserManager() + err = user_manager.SetUser(self.Ctx, &api_proto.VelociraptorUser{ + Name: self.username, + }) + + // Make the user a reader on the root org. + err = services.GrantUserToOrg(self.Ctx, + utils.GetSuperuserName(self.ConfigObj), + self.username, + []string{"root"}, &acl_proto.ApiClientACL{ + // User has no permissions at all! + Roles: []string{}, + }) + assert.NoError(self.T(), err) + + message := &api_proto.PushEventRequest{ + Artifact: "Server.Audit.Logs", + ClientId: constants.VELOCIRAPTOR_SERVER_CLIENT_ID, + Jsonl: append([]byte(`{"foo": "bar"}`), '\n'), + Rows: 1, + } + + // Try to push the event - should not work because user has no + // publish access. + _, err = client.PushEvents(self.Ctx, message) + assert.Error(self.T(), err) + assert.Contains(self.T(), err.Error(), + "Permission denied: PUBLISH TestApiUser to Server.Audit.Logs") + + // Give the user publish access to this queue. + err = services.GrantUserToOrg(self.Ctx, + utils.GetSuperuserName(self.ConfigObj), + self.username, + []string{"root"}, &acl_proto.ApiClientACL{ + // User has no roles at all! but should still be able to + // push to the audit log. + Roles: []string{}, + PublishQueues: []string{"Server.Audit.Logs"}, + }) + assert.NoError(self.T(), err) + + // Try again - should work this time! + _, err = client.PushEvents(self.Ctx, message) + assert.NoError(self.T(), err) + + // Lets check if it is there. + path_manager := artifacts.NewArtifactPathManagerWithMode( + self.ConfigObj, constants.VELOCIRAPTOR_SERVER_CLIENT_ID, "", + "Server.Audit.Logs", artifact_modes.MODE_SERVER_EVENT) + + file_store_factory := file_store.GetFileStore(self.ConfigObj) + rs_reader, err := result_sets.NewResultSetReaderWithOptions( + self.Ctx, self.ConfigObj, file_store_factory, + path_manager.Path(), result_sets.ResultSetOptions{}) + assert.NoError(self.T(), err) + defer rs_reader.Close() + + var rows []*ordereddict.Dict + for row := range rs_reader.Rows(self.Ctx) { + rows = append(rows, row) + } + + assert.Equal(self.T(), len(rows), 1) + + // Make sure the user that sent the event is marked in the event. + sender, _ := rows[0].GetString("_Sender") + assert.Equal(self.T(), sender, self.username) + + // The actual data is stored. + bar, _ := rows[0].GetString("foo") + assert.Equal(self.T(), bar, "bar") +} + +func TestAPI(t *testing.T) { + suite.Run(t, &GeneralAPITest{}) +} diff --git a/api/filesearch.go b/api/filesearch.go new file mode 100644 index 000000000..974e30898 --- /dev/null +++ b/api/filesearch.go @@ -0,0 +1,233 @@ +package api + +import ( + "bytes" + "context" + "encoding/hex" + "io" + "regexp" + "strings" + + errors "github.com/go-errors/errors" + file_store_accessor "www.velocidex.com/golang/velociraptor/accessors/file_store" + "www.velocidex.com/golang/velociraptor/acls" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/file_store" + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/file_store/path_specs" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/uploads" + "www.velocidex.com/golang/velociraptor/utils" +) + +var ( + unsupportedSearchType = errors.New("Unsupported Search Type") +) + +func (self *ApiServer) SearchFile(ctx context.Context, + in *api_proto.SearchFileRequest) (*api_proto.SearchFileResponse, error) { + + defer Instrument("SearchFile")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, user_record.Name, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to search files.") + } + + if len(in.VfsComponents) == 0 { + return nil, PermissionDenied(err, "No file specified") + } + + matcher, err := newMatcher(in.Term, in.Type) + if err != nil { + return nil, Status(self.verbose, err) + } + + path_spec := path_specs.NewUnsafeFilestorePath(in.VfsComponents...). + SetType(api.PATH_TYPE_FILESTORE_ANY) + + err = file_store_accessor.IsFileAccessible(path_spec) + if err != nil { + return nil, Status(self.verbose, err) + } + + file, err := file_store.GetFileStore(org_config_obj).ReadFile(path_spec) + if err != nil { + return nil, Status(self.verbose, err) + } + defer file.Close() + + var reader_at io.ReaderAt = utils.MakeReaderAtter(file) + index, err := getIndex(org_config_obj, path_spec) + + // If the file is sparse, we use the sparse reader. + if err == nil && in.Padding && len(index.Ranges) > 0 { + if !uploads.ShouldPadFile(org_config_obj, index) { + return nil, Status(self.verbose, errors.New( + "Sparse file is too sparse - unable to pad")) + } + + reader_at = &utils.RangedReader{ + ReaderAt: reader_at, + Index: index, + } + } + + offset := int64(in.Offset) + var buf []byte + + if in.Forward { + buf = pool.Get().([]byte) + defer pool.Put(buf) + + // To search backwards we need to rewind before the current + // offset. We may hit the start of the file, in which case we will + // get a smaller buffer. + } else { + base_offset := offset - BUFSIZE + + // The buffer is too small for the pool buffer - just allocate + // it from heap. + if base_offset < 0 { + buf = make([]byte, offset) + base_offset = 0 + + } else { + // Buffer is large enough so we can use the pool. + buf = pool.Get().([]byte) + defer pool.Put(buf) + } + + // Offset now reflects the start of the buffer. + offset = base_offset + } + + for { + // Allow for cancellations + select { + case <-ctx.Done(): + return &api_proto.SearchFileResponse{}, nil + default: + } + + n, err := reader_at.ReadAt(buf, offset) + if err != nil && err != io.EOF { + return nil, Status(self.verbose, err) + } + if n <= 0 { + return &api_proto.SearchFileResponse{}, nil + } + + if in.Forward { + hit := matcher.index(buf[:n]) + if hit >= 0 { + return &api_proto.SearchFileResponse{ + VfsComponents: in.VfsComponents, + Hit: uint64(hit + offset), + }, nil + } + offset += int64(n) + + } else { + hit := matcher.last_index(buf[:n]) + if hit >= 0 { + return &api_proto.SearchFileResponse{ + VfsComponents: in.VfsComponents, + Hit: uint64(hit + offset), + }, nil + } + offset -= int64(n) + + // Offset went backwards before the start of the file - we + // didnt find it. + if offset < 0 { + return &api_proto.SearchFileResponse{}, nil + } + } + } +} + +type matcher interface { + index(buff []byte) int64 + last_index(buff []byte) int64 +} + +type literal_matcher struct { + bytes []byte +} + +func (self *literal_matcher) index(buff []byte) int64 { + return int64(bytes.Index(buff, self.bytes)) +} + +func (self *literal_matcher) last_index(buff []byte) int64 { + return int64(bytes.LastIndex(buff, self.bytes)) +} + +type regex_matcher struct { + regex *regexp.Regexp +} + +func (self *regex_matcher) index(buff []byte) int64 { + match := self.regex.FindIndex(buff) + if len(match) == 0 { + return -1 + } + return int64(match[0]) +} + +// This is not super efficient for now because there is no easy way to +// regex search from the end. +func (self *regex_matcher) last_index(buff []byte) int64 { + matches := self.regex.FindAllIndex(buff, 1000) + if len(matches) == 0 { + return -1 + } + last_match := matches[len(matches)-1] + return int64(last_match[0]) +} + +func newMatcher(term, search_type string) (matcher, error) { + switch search_type { + case "", "string": + return &literal_matcher{[]byte(term)}, nil + + case "regex": + re, err := regexp.Compile("(?ism)" + term) + if err != nil { + return nil, err + } + return ®ex_matcher{re}, nil + + case "hex": + str := strings.Replace(term, " ", "", -1) + + hex, err := hex.DecodeString(strings.TrimPrefix(str, "0x")) + if err != nil { + return nil, err + } + + // If the string has a 0x prefix, we assume it means a little + // endian integer so we need to reverse it. + if strings.HasPrefix(term, "0x") { + reversed := make([]byte, 0, len(hex)) + for i := len(hex) - 1; i >= 0; i-- { + reversed = append(reversed, hex[i]) + } + hex = reversed + } + + return &literal_matcher{hex}, nil + + default: + return nil, unsupportedSearchType + } +} diff --git a/api/fixtures/TestBasicAuthenticator.golden b/api/fixtures/TestBasicAuthenticator.golden new file mode 100644 index 000000000..169f5ea75 --- /dev/null +++ b/api/fixtures/TestBasicAuthenticator.golden @@ -0,0 +1,103 @@ +{ + "Mux": { + "/favicon.png": [ + "*http.redirectHandler" + ], + "/velociraptor/": [ + "api.PrepareGUIMux" + ], + "/velociraptor/api/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.(*BasicAuthenticator).AuthenticateUserHandler", + " *utils.ServeMux" + ], + "/velociraptor/api/v1/DownloadTable": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.(*BasicAuthenticator).AuthenticateUserHandler", + " api.downloadTable" + ], + "/velociraptor/api/v1/DownloadVFSFile": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.(*BasicAuthenticator).AuthenticateUserHandler", + " api.vfsFileDownloadHandler" + ], + "/velociraptor/api/v1/UploadFormFile": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.(*BasicAuthenticator).AuthenticateUserHandler", + " api.formUploadHandler" + ], + "/velociraptor/api/v1/UploadTool": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.(*BasicAuthenticator).AuthenticateUserHandler", + " api.toolUploadHandler" + ], + "/velociraptor/app/": [ + "authenticators.IpFilter", + " utils.StripPrefix", + " NewInterceptingResponseWriter", + " api.fixCSSURLs", + " *gzipped.fileHandler" + ], + "/velociraptor/app/index.html": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.(*BasicAuthenticator).AuthenticateUserHandler", + " api.GetTemplateHandler" + ], + "/velociraptor/app/logoff.html": [ + "authenticators.IpFilter", + " authenticators.(*BasicAuthenticator).AddLogoff" + ], + "/velociraptor/clients/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.(*BasicAuthenticator).AuthenticateUserHandler", + " utils.StripPrefix", + " api.downloadFileStore" + ], + "/velociraptor/debug/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.(*BasicAuthenticator).AuthenticateUserHandler", + " utils.StripPrefix", + " *server.debugMux" + ], + "/velociraptor/downloads/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.(*BasicAuthenticator).AuthenticateUserHandler", + " utils.StripPrefix", + " api.downloadFileStore" + ], + "/velociraptor/hunts/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.(*BasicAuthenticator).AuthenticateUserHandler", + " utils.StripPrefix", + " api.downloadFileStore" + ], + "/velociraptor/notebooks/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.(*BasicAuthenticator).AuthenticateUserHandler", + " utils.StripPrefix", + " api.downloadFileStore" + ] + } +} \ No newline at end of file diff --git a/api/fixtures/TestMultiAuthenticator.golden b/api/fixtures/TestMultiAuthenticator.golden new file mode 100644 index 000000000..49921d6f2 --- /dev/null +++ b/api/fixtures/TestMultiAuthenticator.golden @@ -0,0 +1,139 @@ +{ + "Redirect Provider *authenticators.OidcAuthenticator Generic OIDC Connector": "https://www.example.com/velociraptor/auth/oidc/callback", + "Redirect Provider *authenticators.OidcAuthenticator Google": "https://www.example.com/velociraptor/auth/google/callback", + "Redirect Provider *authenticators.OidcAuthenticator GitHub": "https://www.example.com/velociraptor/auth/github/callback", + "Redirect Provider *authenticators.OidcAuthenticator Azure": "https://www.example.com/velociraptor/auth/azure/callback", + "Mux": { + "/favicon.png": [ + "*http.redirectHandler" + ], + "/velociraptor/": [ + "api.PrepareGUIMux" + ], + "/velociraptor/api/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.authenticateUserHandle", + " *utils.ServeMux" + ], + "/velociraptor/api/v1/DownloadTable": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.authenticateUserHandle", + " api.downloadTable" + ], + "/velociraptor/api/v1/DownloadVFSFile": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.authenticateUserHandle", + " api.vfsFileDownloadHandler" + ], + "/velociraptor/api/v1/UploadFormFile": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.authenticateUserHandle", + " api.formUploadHandler" + ], + "/velociraptor/api/v1/UploadTool": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.authenticateUserHandle", + " api.toolUploadHandler" + ], + "/velociraptor/app/": [ + "authenticators.IpFilter", + " utils.StripPrefix", + " NewInterceptingResponseWriter", + " api.fixCSSURLs", + " *gzipped.fileHandler" + ], + "/velociraptor/app/index.html": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.authenticateUserHandle", + " api.GetTemplateHandler" + ], + "/velociraptor/app/logoff.html": [ + "authenticators.IpFilter", + " authenticators.installLogoff" + ], + "/velociraptor/auth/azure/callback": [ + "authenticators.IpFilter", + " authenticators.(*OidcAuthenticator).oauthOidcCallback" + ], + "/velociraptor/auth/azure/login": [ + "authenticators.IpFilter", + " authenticators.(*OidcAuthenticator).oauthOidcLogin" + ], + "/velociraptor/auth/github/callback": [ + "authenticators.IpFilter", + " authenticators.(*OidcAuthenticator).oauthOidcCallback" + ], + "/velociraptor/auth/github/login": [ + "authenticators.IpFilter", + " authenticators.(*OidcAuthenticator).oauthOidcLogin" + ], + "/velociraptor/auth/google/callback": [ + "authenticators.IpFilter", + " authenticators.(*OidcAuthenticator).oauthOidcCallback" + ], + "/velociraptor/auth/google/login": [ + "authenticators.IpFilter", + " authenticators.(*OidcAuthenticator).oauthOidcLogin" + ], + "/velociraptor/auth/oidc/callback": [ + "authenticators.IpFilter", + " authenticators.(*OidcAuthenticator).oauthOidcCallback" + ], + "/velociraptor/auth/oidc/login": [ + "authenticators.IpFilter", + " authenticators.(*OidcAuthenticator).oauthOidcLogin" + ], + "/velociraptor/clients/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.authenticateUserHandle", + " utils.StripPrefix", + " api.downloadFileStore" + ], + "/velociraptor/debug/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.authenticateUserHandle", + " utils.StripPrefix", + " *server.debugMux" + ], + "/velociraptor/downloads/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.authenticateUserHandle", + " utils.StripPrefix", + " api.downloadFileStore" + ], + "/velociraptor/hunts/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.authenticateUserHandle", + " utils.StripPrefix", + " api.downloadFileStore" + ], + "/velociraptor/notebooks/": [ + "authenticators.IpFilter", + " api.csrfProtect", + " GetLoggingHandler", + " authenticators.authenticateUserHandle", + " utils.StripPrefix", + " api.downloadFileStore" + ] + } +} \ No newline at end of file diff --git a/api/flows.go b/api/flows.go new file mode 100644 index 000000000..7717f2ec6 --- /dev/null +++ b/api/flows.go @@ -0,0 +1,202 @@ +package api + +import ( + "context" + + "github.com/Velocidex/ordereddict" + "google.golang.org/protobuf/types/known/emptypb" + "www.velocidex.com/golang/velociraptor/acls" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/api/tables" + artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" + "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/json" + vjson "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/services" +) + +func (self *ApiServer) CancelFlow( + ctx context.Context, + in *api_proto.ApiFlowRequest) (*api_proto.StartFlowResponse, error) { + + defer Instrument("CancelFlow")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + permissions := acls.COLLECT_CLIENT + if in.ClientId == constants.VELOCIRAPTOR_SERVER_CLIENT_ID { + permissions = acls.COLLECT_SERVER + } + + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to cancel flows.") + } + + launcher, err := services.GetLauncher(org_config_obj) + if err != nil { + return nil, err + } + result, err := launcher.CancelFlow( + ctx, org_config_obj, in.ClientId, in.FlowId, principal) + if err != nil { + return nil, Status(self.verbose, err) + } + + // Log this event as and Audit event. + err = services.LogAudit(ctx, + org_config_obj, principal, "CancelFlow", + ordereddict.NewDict(). + Set("client", in.ClientId). + Set("flow_id", in.FlowId). + Set("details", in)) + + return result, err +} + +func (self *ApiServer) ResumeFlow( + ctx context.Context, + in *api_proto.ApiFlowRequest) (*emptypb.Empty, error) { + + defer Instrument("ResumeFlow")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + permissions := acls.COLLECT_CLIENT + if in.ClientId == constants.VELOCIRAPTOR_SERVER_CLIENT_ID { + permissions = acls.COLLECT_SERVER + } + + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to resume flows.") + } + + launcher, err := services.GetLauncher(org_config_obj) + if err != nil { + return nil, err + } + _, err = launcher.ResumeFlow( + ctx, org_config_obj, in.ClientId, in.FlowId) + if err != nil { + return nil, Status(self.verbose, err) + } + + // Log this event as and Audit event. + err = services.LogAudit(ctx, + org_config_obj, principal, "ResumeFlow", + ordereddict.NewDict(). + Set("client", in.ClientId). + Set("flow_id", in.FlowId). + Set("details", in)) + + return &emptypb.Empty{}, err +} + +func (self *ApiServer) GetClientFlows( + ctx context.Context, + in *api_proto.GetTableRequest) (*api_proto.GetTableResponse, error) { + + defer Instrument("GetClientFlows")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + user_name := user_record.Name + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, user_name, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to view flows.") + } + + launcher, err := services.GetLauncher(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + // If no sort column is specified, sort by flow id so later flows + // are on top. Flow Ids have times encoded in them so they sort + // chronologically. + if in.SortColumn == "" { + in.SortColumn = "FlowId" + in.SortDirection = true + } + + options, err := tables.GetTableOptions(in) + if err != nil { + return nil, Status(self.verbose, err) + } + + flows, err := launcher.GetFlows(ctx, org_config_obj, in.ClientId, options, + int64(in.StartRow), int64(in.Rows)) + if err != nil { + return nil, Status(self.verbose, err) + } + + result := &api_proto.GetTableResponse{ + TotalRows: int64(flows.Total), + Columns: []string{ + "State", "FlowId", "Artifacts", "Created", "Last Active", "Creator", + "Mb", "Rows", "_Flow", "_Urgent", "_ArtifactsWithResults", + }, + ColumnTypes: []*artifacts_proto.ColumnType{{ + Name: "Created", + Type: "timestamp", + }, { + Name: "Last Active", + Type: "timestamp", + }, { + Name: "Mb", + Type: "mb", + }, { + Name: "Rows", + Type: "number", + }}, + } + + // Convert the items into a table format + for _, flow := range flows.Items { + if flow.Request == nil { + continue + } + row_data := []interface{}{ + flow.State.String(), + flow.SessionId, + flow.Request.Artifacts, + flow.CreateTime, + flow.ActiveTime, + flow.Request.Creator, + flow.TotalUploadedBytes, + flow.TotalCollectedRows, + json.ConvertProtoToOrderedDict(flow), + flow.Request.Urgent, + flow.ArtifactsWithResults, + } + opts := vjson.DefaultEncOpts() + serialized, err := json.MarshalWithOptions(row_data, opts) + if err != nil { + continue + } + result.Rows = append(result.Rows, &api_proto.Row{ + Json: string(serialized), + }) + } + + return result, nil +} diff --git a/api/handlers.go b/api/handlers.go index bc5ac4ff7..08bf16b7d 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -1,27 +1,25 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package api import ( "context" - "net/http" - "github.com/sirupsen/logrus" api_proto "www.velocidex.com/golang/velociraptor/api/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/constants" @@ -29,27 +27,6 @@ import ( "www.velocidex.com/golang/velociraptor/logging" ) -// Record the status of the request so we can log it. -type statusRecorder struct { - http.ResponseWriter - http.Flusher - status int - error []byte -} - -func (self *statusRecorder) WriteHeader(code int) { - self.status = code - self.ResponseWriter.WriteHeader(code) -} - -func (self *statusRecorder) Write(buf []byte) (int, error) { - if self.status == 500 { - self.error = buf - } - - return self.ResponseWriter.Write(buf) -} - func GetUserInfo(ctx context.Context, config_obj *config_proto.Config) *api_proto.VelociraptorUser { result := &api_proto.VelociraptorUser{} @@ -65,43 +42,3 @@ func GetUserInfo(ctx context.Context, } return result } - -func GetLoggingHandler(config_obj *config_proto.Config) func(http.Handler) http.Handler { - logger := logging.GetLogger(config_obj, &logging.GUIComponent) - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rec := &statusRecorder{ - w, - w.(http.Flusher), - 200, nil} - defer func() { - if rec.status == 500 { - logger.WithFields( - logrus.Fields{ - "method": r.Method, - "url": r.URL.Path, - "remote": r.RemoteAddr, - "error": string(rec.error), - "user-agent": r.UserAgent(), - "status": rec.status, - "user": GetUserInfo( - r.Context(), config_obj).Name, - }).Error("") - - } else { - logger.WithFields( - logrus.Fields{ - "method": r.Method, - "url": r.URL.Path, - "remote": r.RemoteAddr, - "user-agent": r.UserAgent(), - "status": rec.status, - "user": GetUserInfo( - r.Context(), config_obj).Name, - }).Info("") - } - }() - next.ServeHTTP(rec, r) - }) - } -} diff --git a/api/health.go b/api/health.go index 727991aa1..1178af236 100644 --- a/api/health.go +++ b/api/health.go @@ -1,7 +1,8 @@ package api import ( - context "golang.org/x/net/context" + "context" + "www.velocidex.com/golang/velociraptor/api/proto" api_proto "www.velocidex.com/golang/velociraptor/api/proto" ) diff --git a/api/hunts.go b/api/hunts.go index 7a4cd4e6b..c0a0d8176 100644 --- a/api/hunts.go +++ b/api/hunts.go @@ -1,81 +1,179 @@ package api import ( + "context" "fmt" + "strings" + "time" "github.com/Velocidex/ordereddict" - "github.com/sirupsen/logrus" - context "golang.org/x/net/context" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + errors "github.com/go-errors/errors" + + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/emptypb" "www.velocidex.com/golang/velociraptor/acls" api_proto "www.velocidex.com/golang/velociraptor/api/proto" - file_store "www.velocidex.com/golang/velociraptor/file_store" - "www.velocidex.com/golang/velociraptor/file_store/csv" - "www.velocidex.com/golang/velociraptor/flows" + "www.velocidex.com/golang/velociraptor/api/tables" + "www.velocidex.com/golang/velociraptor/json" + vjson "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/logging" - "www.velocidex.com/golang/velociraptor/paths" - "www.velocidex.com/golang/velociraptor/result_sets" - "www.velocidex.com/golang/velociraptor/search" "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/services/hunt_dispatcher" "www.velocidex.com/golang/velociraptor/utils" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" ) func (self *ApiServer) GetHuntFlows( ctx context.Context, in *api_proto.GetTableRequest) (*api_proto.GetTableResponse, error) { - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + defer Instrument("GetHuntFlows")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view hunt results.") } - hunt_path_manager := paths.NewHuntPathManager(in.HuntId).Clients() - file_store_factory := file_store.GetFileStore(self.config) - rs_reader, err := result_sets.NewResultSetReader( - file_store_factory, hunt_path_manager) + options, err := tables.GetTableOptions(in) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - defer rs_reader.Close() - // Seek to the row we need. - err = rs_reader.SeekToRow(int64(in.StartRow)) + hunt_dispatcher, err := services.GetHuntDispatcher(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) + } + + scope := vql_subsystem.MakeScope() + flow_chan, total_rows, err := hunt_dispatcher.GetFlows( + ctx, org_config_obj, + services.FlowSearchOptions{ + ResultSetOptions: options, + }, + scope, in.HuntId, int(in.StartRow)) + if err != nil { + return nil, Status(self.verbose, err) } result := &api_proto.GetTableResponse{ - TotalRows: rs_reader.TotalRows(), + TotalRows: total_rows, Columns: []string{ "ClientId", "Hostname", "FlowId", "StartedTime", "State", "Duration", "TotalBytes", "TotalRows", }} - for row := range rs_reader.Rows(ctx) { - client_id := utils.GetString(row, "ClientId") - flow_id := utils.GetString(row, "FlowId") - flow, err := flows.LoadCollectionContext(self.config, client_id, flow_id) + for flow := range flow_chan { + if flow.Context == nil { + continue + } + + row_data := []interface{}{ + flow.Context.ClientId, + services.GetHostname(ctx, org_config_obj, flow.Context.ClientId), + flow.Context.SessionId, + flow.Context.StartTime / 1000, + flow.Context.State.String(), + flow.Context.ExecutionDuration / 1000000000, + flow.Context.TotalUploadedBytes, + flow.Context.TotalCollectedRows, + } + + opts := vjson.DefaultEncOpts() + serialized, err := json.MarshalWithOptions(row_data, opts) if err != nil { continue } - row_data := []string{ - client_id, - services.GetHostname(client_id), - flow_id, - csv.AnyToString(flow.StartTime / 1000), - flow.State.String(), - csv.AnyToString(flow.ExecutionDuration / 1000000000), - csv.AnyToString(flow.TotalUploadedBytes), - csv.AnyToString(flow.TotalCollectedRows)} + result.Rows = append(result.Rows, &api_proto.Row{ + Json: string(serialized), + }) + + if uint64(len(result.Rows)) > in.Rows { + break + } + } + return result, nil +} + +func (self *ApiServer) GetHuntTable( + ctx context.Context, + in *api_proto.GetTableRequest) (*api_proto.GetTableResponse, error) { + + defer Instrument("GetHuntTable")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to view hunt results.") + } + + hunt_dispatcher, err := services.GetHuntDispatcher(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + options, err := tables.GetTableOptions(in) + if err != nil { + return nil, Status(self.verbose, err) + } + + hunts, total, err := hunt_dispatcher.GetHunts(ctx, org_config_obj, options, + int64(in.StartRow), int64(in.Rows)) + if err != nil { + return nil, Status(self.verbose, err) + } + + result := &api_proto.GetTableResponse{ + TotalRows: total, + Columns: []string{ + "State", "Tags", "HuntId", + "Description", "Created", + "Started", "Expires", "Scheduled", "Creator", + }} - result.Rows = append(result.Rows, &api_proto.Row{Cell: row_data}) + for _, hunt := range hunts { + var total_clients_scheduled uint64 + if hunt.Stats != nil { + total_clients_scheduled = hunt.Stats.TotalClientsScheduled + } + + row_data := []interface{}{ + fmt.Sprintf("%v", hunt.State), + hunt.Tags, + hunt.HuntId, + hunt.HuntDescription, + hunt.CreateTime, + hunt.StartTime, + hunt.Expires, + total_clients_scheduled, + hunt.Creator, + } + opts := vjson.DefaultEncOpts() + serialized, err := json.MarshalWithOptions(row_data, opts) + if err != nil { + continue + } + result.Rows = append(result.Rows, &api_proto.Row{ + Json: string(serialized), + }) if uint64(len(result.Rows)) > in.Rows { break @@ -90,64 +188,186 @@ func (self *ApiServer) CreateHunt( defer Instrument("CreateHunt")() + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + // Log this event as an Audit event. - in.Creator = GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - in.HuntId = flows.GetNewHuntId() + in.Creator = principal + in.HuntId = hunt_dispatcher.GetNewHuntId() - acl_manager := vql_subsystem.NewServerACLManager(self.config, in.Creator) + acl_manager := acl_managers.NewServerACLManager(org_config_obj, in.Creator) + // It is possible to start a paused hunt with the COLLECT_CLIENT + // permission. permissions := acls.COLLECT_CLIENT - perm, err := acls.CheckAccess(self.config, in.Creator, permissions) + + // To actually start the hunt we need the START_HUNT + // permission. This allows for division of responsibility between + // hunt proposers and hunt starters. + if in.State == api_proto.Hunt_RUNNING { + permissions = acls.START_HUNT + } + + perm, err := services.CheckAccess(org_config_obj, in.Creator, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to launch hunts.") } - logging.GetLogger(self.config, &logging.Audit). - WithFields(logrus.Fields{ - "user": in.Creator, - "hunt_id": in.HuntId, - "details": fmt.Sprintf("%v", in), - }).Info("CreateHunt") + // Require the Org Admin permission to launch hunts in a differen + // org. + orgs := in.OrgIds + if len(orgs) > 0 { + permissions := acls.ORG_ADMIN + perm, err := services.CheckAccess(org_config_obj, in.Creator, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to launch hunts in other orgs.") + } + } else { + orgs = append(orgs, org_config_obj.OrgId) + } - result := &api_proto.StartFlowResponse{} - hunt_id, err := flows.CreateHunt( - ctx, self.config, acl_manager, in) + org_manager, err := services.GetOrgManager() if err != nil { - return nil, err + return nil, Status(self.verbose, err) + } + + var orgs_we_scheduled []string + var errors_msg []string + + for _, org_id := range orgs { + org_config_obj, err := org_manager.GetOrgConfig(org_id) + if err != nil { + errors_msg = append(errors_msg, fmt.Sprintf("In Org %v: GetOrgConfig %v", org_id, err)) + continue + } + + // Make sure the user is allowed to collect in that org + perm, err := services.CheckAccess( + org_config_obj, in.Creator, permissions) + if !perm { + if err != nil { + errors_msg = append(errors_msg, fmt.Sprintf( + "%v: CreateHunt: User is not allowed to launch hunts in "+ + "org %v.", err, org_id)) + } else { + errors_msg = append(errors_msg, fmt.Sprintf( + "CreateHunt: User is not allowed to launch hunts in "+ + "org %v.", org_id)) + } + continue + } + + hunt_dispatcher, err := services.GetHuntDispatcher(org_config_obj) + if err != nil { + errors_msg = append(errors_msg, fmt.Sprintf( + "%v: CreateHunt: GetOrgConfig %v", org_id, err)) + continue + } + + // In the root org mark the org ids that we are launching. + org_hunt_request := proto.Clone(in).(*api_proto.Hunt) + if !utils.IsRootOrg(org_id) { + org_hunt_request.OrgIds = nil + } + + new_hunt, err := hunt_dispatcher.CreateHunt( + ctx, org_config_obj, acl_manager, org_hunt_request) + if err != nil { + errors_msg = append(errors_msg, fmt.Sprintf( + "%v: CreateHunt: GetOrgConfig %v", org_id, err)) + continue + } + + orgs_we_scheduled = append(orgs_we_scheduled, org_id) + // Reuse the hunt id for all the hunts we launch on all the + // orgs - this makes it easier to combine results from all + // orgs. + in.HuntId = new_hunt.HuntId } - result.FlowId = hunt_id + if len(errors_msg) != 0 { + return nil, Status(self.verbose, + errors.New(strings.Join(errors_msg, "\n"))) + } + + result := &api_proto.StartFlowResponse{} + result.FlowId = in.HuntId + + // Audit message for GUI access + err = services.LogAudit(ctx, + org_config_obj, principal, "CreateHunt", + ordereddict.NewDict(). + Set("hunt_id", result.FlowId). + Set("details", in). + Set("orgs", orgs_we_scheduled)) + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.FrontendComponent) + logger.Error("CreateHunt %v %v", principal, result.FlowId) + } return result, nil } func (self *ApiServer) ModifyHunt( ctx context.Context, - in *api_proto.Hunt) (*emptypb.Empty, error) { + in *api_proto.HuntMutation) (*emptypb.Empty, error) { defer Instrument("ModifyHunt")() // Log this event as an Audit event. - in.Creator = GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name permissions := acls.COLLECT_CLIENT - perm, err := acls.CheckAccess(self.config, in.Creator, permissions) + if in.State == api_proto.Hunt_RUNNING { + permissions = acls.START_HUNT + } + + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to modify hunts.") } - logging.GetLogger(self.config, &logging.Audit). - WithFields(logrus.Fields{ - "user": in.Creator, - "hunt_id": in.HuntId, - "details": fmt.Sprintf("%v", in), - }).Info("ModifyHunt") + err = services.LogAudit(ctx, + org_config_obj, principal, "ModifyHunt", + ordereddict.NewDict(). + Set("hunt_id", in.HuntId). + Set("details", in)) + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.FrontendComponent) + logger.Error("ModifyHunt %v %v", principal, in.HuntId) + } - err = flows.ModifyHunt(ctx, self.config, in, in.Creator) + hunt_dispatcher, err := services.GetHuntDispatcher(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) + } + + // Only allow some fields to be set by the GUI + mutation := &api_proto.HuntMutation{ + HuntId: in.HuntId, + State: in.State, + Description: in.Description, + Stats: in.Stats, + Expires: in.Expires, + Tags: in.Tags, + User: principal, + } + + err = hunt_dispatcher.MutateHunt(ctx, org_config_obj, mutation) + if err != nil { + return nil, Status(self.verbose, err) } result := &emptypb.Empty{} @@ -160,17 +380,29 @@ func (self *ApiServer) ListHunts( defer Instrument("ListHunts")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view hunts.") } - result, err := flows.ListHunts(self.config, in) + hunt_dispatcher, err := services.GetHuntDispatcher(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + result, err := hunt_dispatcher.ListHunts( + ctx, org_config_obj, in) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } // Provide only a summary for list hunts GUI @@ -204,33 +436,84 @@ func (self *ApiServer) GetHunt( defer Instrument("GetHunt")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view hunts.") } - result, err := flows.GetHunt(self.config, in) + hunt_dispatcher, err := services.GetHuntDispatcher(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) + } + + result, pres := hunt_dispatcher.GetHunt(ctx, in.HuntId) + if !pres { + return nil, Status(self.verbose, + fmt.Errorf("%w: %v", services.HuntNotFoundError, in.HuntId)) + } + + if !in.IncludeRequest && result.StartRequest != nil { + result.StartRequest.CompiledCollectorArgs = nil } return result, nil } +func (self *ApiServer) GetHuntTags( + ctx context.Context, + in *emptypb.Empty) (*api_proto.HuntTags, error) { + defer Instrument("GetHuntTags")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to view hunts.") + } + + hunt_dispatcher, err := services.GetHuntDispatcher(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + return &api_proto.HuntTags{ + Tags: hunt_dispatcher.GetTags(ctx), + }, nil +} + func (self *ApiServer) GetHuntResults( ctx context.Context, in *api_proto.GetHuntResultsRequest) (*api_proto.GetTableResponse, error) { defer Instrument("GetHuntResults")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view results.") } @@ -241,11 +524,11 @@ func (self *ApiServer) GetHuntResults( // More than 100 results are not very useful in the GUI - // users should just download the json file for post // processing or process in the notebook. - result, err := RunVQL(ctx, self.config, user_name, env, + result, err := RunVQL(ctx, org_config_obj, principal, env, "SELECT * FROM hunt_results(hunt_id=HuntID, "+ "artifact=ArtifactName) LIMIT 100") if err != nil { - return nil, err + return nil, Status(self.verbose, err) } return result, nil @@ -253,18 +536,48 @@ func (self *ApiServer) GetHuntResults( func (self *ApiServer) EstimateHunt( ctx context.Context, - in *api_proto.Hunt) (*api_proto.HuntStats, error) { + in *api_proto.HuntEstimateRequest) (*api_proto.HuntStats, error) { defer Instrument("EstimateHunt")() + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view hunt results.") } + client_info_manager, err := services.GetClientInfoManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + indexer, err := services.GetIndexer(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + now := uint64(time.Now().UnixNano() / 1000) + + is_client_recent := func(client_id string, seen map[string]bool) { + // We dont care about last active status + if in.LastActive == 0 { + seen[client_id] = true + return + } + + stats, err := client_info_manager.GetStats(ctx, client_id) + if err == nil && now-in.LastActive*1000000 < stats.Ping { + seen[client_id] = true + } + } + if in.Condition != nil { labels := in.Condition.GetLabels() if labels != nil && len(labels.Label) > 0 { @@ -272,17 +585,17 @@ func (self *ApiServer) EstimateHunt( // has any of the labels set, it will be scheduled. seen := make(map[string]bool) for _, label := range labels.Label { - for entity := range search.SearchIndexWithPrefix( - ctx, self.config, "label:"+label) { - seen[entity.Entity] = true + for entity := range indexer.SearchIndexWithPrefix( + ctx, org_config_obj, "label:"+label) { + is_client_recent(entity.Entity, seen) } } // Remove any excluded labels. if in.Condition.ExcludedLabels != nil { for _, label := range in.Condition.ExcludedLabels.Label { - for entity := range search.SearchIndexWithPrefix( - ctx, self.config, "label:"+label) { + for entity := range indexer.SearchIndexWithPrefix( + ctx, org_config_obj, "label:"+label) { delete(seen, entity.Entity) } } @@ -307,18 +620,18 @@ func (self *ApiServer) EstimateHunt( os_name = "darwin" } - client_info_manager, err := services.GetClientInfoManager() + client_info_manager, err := services.GetClientInfoManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - for hit := range search.SearchIndexWithPrefix(ctx, - self.config, "all") { + for hit := range indexer.SearchIndexWithPrefix(ctx, + org_config_obj, "all") { client_id := hit.Entity - client_info, err := client_info_manager.Get(client_id) + client_info, err := client_info_manager.Get(ctx, client_id) if err == nil { if os_name == client_info.System { - seen[hit.Entity] = true + is_client_recent(hit.Entity, seen) } } } @@ -326,8 +639,8 @@ func (self *ApiServer) EstimateHunt( // Remove any excluded labels. if in.Condition.ExcludedLabels != nil { for _, label := range in.Condition.ExcludedLabels.Label { - for entity := range search.SearchIndexWithPrefix( - ctx, self.config, "label:"+label) { + for entity := range indexer.SearchIndexWithPrefix( + ctx, org_config_obj, "label:"+label) { delete(seen, entity.Entity) } } @@ -340,15 +653,15 @@ func (self *ApiServer) EstimateHunt( // No condition, just count all the clients. seen := make(map[string]bool) - for hit := range search.SearchIndexWithPrefix(ctx, self.config, "all") { - seen[hit.Entity] = true + for hit := range indexer.SearchIndexWithPrefix(ctx, org_config_obj, "all") { + is_client_recent(hit.Entity, seen) } // Remove any excluded labels. if in.Condition.ExcludedLabels != nil { for _, label := range in.Condition.ExcludedLabels.Label { - for entity := range search.SearchIndexWithPrefix( - ctx, self.config, "label:"+label) { + for entity := range indexer.SearchIndexWithPrefix( + ctx, org_config_obj, "label:"+label) { delete(seen, entity.Entity) } } @@ -361,8 +674,8 @@ func (self *ApiServer) EstimateHunt( // No condition, just count all the clients. seen := make(map[string]bool) - for hit := range search.SearchIndexWithPrefix(ctx, self.config, "all") { - seen[hit.Entity] = true + for hit := range indexer.SearchIndexWithPrefix(ctx, org_config_obj, "all") { + is_client_recent(hit.Entity, seen) } return &api_proto.HuntStats{ diff --git a/api/mock/api_mock.go b/api/mock/api_mock.go index 14a851490..9fd0c8fbc 100644 --- a/api/mock/api_mock.go +++ b/api/mock/api_mock.go @@ -41,24 +41,44 @@ func (m *MockAPIClient) EXPECT() *MockAPIClientMockRecorder { return m.recorder } -// ArchiveFlow mocks base method. -func (m *MockAPIClient) ArchiveFlow(arg0 context.Context, arg1 *proto0.ApiFlowRequest, arg2 ...grpc.CallOption) (*proto0.StartFlowResponse, error) { +// AddSecret mocks base method. +func (m *MockAPIClient) AddSecret(arg0 context.Context, arg1 *proto0.Secret, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } - ret := m.ctrl.Call(m, "ArchiveFlow", varargs...) - ret0, _ := ret[0].(*proto0.StartFlowResponse) + ret := m.ctrl.Call(m, "AddSecret", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddSecret indicates an expected call of AddSecret. +func (mr *MockAPIClientMockRecorder) AddSecret(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSecret", reflect.TypeOf((*MockAPIClient)(nil).AddSecret), varargs...) +} + +// AnnotateTimeline mocks base method. +func (m *MockAPIClient) AnnotateTimeline(arg0 context.Context, arg1 *proto0.AnnotationRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AnnotateTimeline", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) ret1, _ := ret[1].(error) return ret0, ret1 } -// ArchiveFlow indicates an expected call of ArchiveFlow. -func (mr *MockAPIClientMockRecorder) ArchiveFlow(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +// AnnotateTimeline indicates an expected call of AnnotateTimeline. +func (mr *MockAPIClientMockRecorder) AnnotateTimeline(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ArchiveFlow", reflect.TypeOf((*MockAPIClient)(nil).ArchiveFlow), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AnnotateTimeline", reflect.TypeOf((*MockAPIClient)(nil).AnnotateTimeline), varargs...) } // CancelFlow mocks base method. @@ -201,64 +221,84 @@ func (mr *MockAPIClientMockRecorder) CreateNotebookDownloadFile(arg0, arg1 inter return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNotebookDownloadFile", reflect.TypeOf((*MockAPIClient)(nil).CreateNotebookDownloadFile), varargs...) } -// DeleteSubject mocks base method. -func (m *MockAPIClient) DeleteSubject(arg0 context.Context, arg1 *proto0.DataRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { +// CreateUser mocks base method. +func (m *MockAPIClient) CreateUser(arg0 context.Context, arg1 *proto0.UpdateUserRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } - ret := m.ctrl.Call(m, "DeleteSubject", varargs...) + ret := m.ctrl.Call(m, "CreateUser", varargs...) ret0, _ := ret[0].(*emptypb.Empty) ret1, _ := ret[1].(error) return ret0, ret1 } -// DeleteSubject indicates an expected call of DeleteSubject. -func (mr *MockAPIClientMockRecorder) DeleteSubject(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +// CreateUser indicates an expected call of CreateUser. +func (mr *MockAPIClientMockRecorder) CreateUser(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubject", reflect.TypeOf((*MockAPIClient)(nil).DeleteSubject), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockAPIClient)(nil).CreateUser), varargs...) } -// EstimateHunt mocks base method. -func (m *MockAPIClient) EstimateHunt(arg0 context.Context, arg1 *proto0.Hunt, arg2 ...grpc.CallOption) (*proto0.HuntStats, error) { +// DeleteNotebook mocks base method. +func (m *MockAPIClient) DeleteNotebook(arg0 context.Context, arg1 *proto0.NotebookMetadata, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } - ret := m.ctrl.Call(m, "EstimateHunt", varargs...) - ret0, _ := ret[0].(*proto0.HuntStats) + ret := m.ctrl.Call(m, "DeleteNotebook", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) ret1, _ := ret[1].(error) return ret0, ret1 } -// EstimateHunt indicates an expected call of EstimateHunt. -func (mr *MockAPIClientMockRecorder) EstimateHunt(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +// DeleteNotebook indicates an expected call of DeleteNotebook. +func (mr *MockAPIClientMockRecorder) DeleteNotebook(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimateHunt", reflect.TypeOf((*MockAPIClient)(nil).EstimateHunt), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNotebook", reflect.TypeOf((*MockAPIClient)(nil).DeleteNotebook), varargs...) } -// ExportNotebook mocks base method. -func (m *MockAPIClient) ExportNotebook(arg0 context.Context, arg1 *proto0.NotebookExportRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { +// DeleteSubject mocks base method. +func (m *MockAPIClient) DeleteSubject(arg0 context.Context, arg1 *proto0.DataRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } - ret := m.ctrl.Call(m, "ExportNotebook", varargs...) + ret := m.ctrl.Call(m, "DeleteSubject", varargs...) ret0, _ := ret[0].(*emptypb.Empty) ret1, _ := ret[1].(error) return ret0, ret1 } -// ExportNotebook indicates an expected call of ExportNotebook. -func (mr *MockAPIClientMockRecorder) ExportNotebook(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +// DeleteSubject indicates an expected call of DeleteSubject. +func (mr *MockAPIClientMockRecorder) DeleteSubject(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubject", reflect.TypeOf((*MockAPIClient)(nil).DeleteSubject), varargs...) +} + +// EstimateHunt mocks base method. +func (m *MockAPIClient) EstimateHunt(arg0 context.Context, arg1 *proto0.HuntEstimateRequest, arg2 ...grpc.CallOption) (*proto0.HuntStats, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "EstimateHunt", varargs...) + ret0, _ := ret[0].(*proto0.HuntStats) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EstimateHunt indicates an expected call of EstimateHunt. +func (mr *MockAPIClientMockRecorder) EstimateHunt(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportNotebook", reflect.TypeOf((*MockAPIClient)(nil).ExportNotebook), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimateHunt", reflect.TypeOf((*MockAPIClient)(nil).EstimateHunt), varargs...) } // GetArtifactFile mocks base method. @@ -322,14 +362,14 @@ func (mr *MockAPIClientMockRecorder) GetClient(arg0, arg1 interface{}, arg2 ...i } // GetClientFlows mocks base method. -func (m *MockAPIClient) GetClientFlows(arg0 context.Context, arg1 *proto0.ApiFlowRequest, arg2 ...grpc.CallOption) (*proto0.ApiFlowResponse, error) { +func (m *MockAPIClient) GetClientFlows(arg0 context.Context, arg1 *proto0.GetTableRequest, arg2 ...grpc.CallOption) (*proto0.GetTableResponse, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "GetClientFlows", varargs...) - ret0, _ := ret[0].(*proto0.ApiFlowResponse) + ret0, _ := ret[0].(*proto0.GetTableResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -421,6 +461,26 @@ func (mr *MockAPIClientMockRecorder) GetFlowRequests(arg0, arg1 interface{}, arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFlowRequests", reflect.TypeOf((*MockAPIClient)(nil).GetFlowRequests), varargs...) } +// GetGlobalUsers mocks base method. +func (m *MockAPIClient) GetGlobalUsers(arg0 context.Context, arg1 *emptypb.Empty, arg2 ...grpc.CallOption) (*proto0.Users, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetGlobalUsers", varargs...) + ret0, _ := ret[0].(*proto0.Users) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGlobalUsers indicates an expected call of GetGlobalUsers. +func (mr *MockAPIClientMockRecorder) GetGlobalUsers(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGlobalUsers", reflect.TypeOf((*MockAPIClient)(nil).GetGlobalUsers), varargs...) +} + // GetHunt mocks base method. func (m *MockAPIClient) GetHunt(arg0 context.Context, arg1 *proto0.GetHuntRequest, arg2 ...grpc.CallOption) (*proto0.Hunt, error) { m.ctrl.T.Helper() @@ -481,6 +541,46 @@ func (mr *MockAPIClientMockRecorder) GetHuntResults(arg0, arg1 interface{}, arg2 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHuntResults", reflect.TypeOf((*MockAPIClient)(nil).GetHuntResults), varargs...) } +// GetHuntTable mocks base method. +func (m *MockAPIClient) GetHuntTable(arg0 context.Context, arg1 *proto0.GetTableRequest, arg2 ...grpc.CallOption) (*proto0.GetTableResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetHuntTable", varargs...) + ret0, _ := ret[0].(*proto0.GetTableResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHuntTable indicates an expected call of GetHuntTable. +func (mr *MockAPIClientMockRecorder) GetHuntTable(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHuntTable", reflect.TypeOf((*MockAPIClient)(nil).GetHuntTable), varargs...) +} + +// GetHuntTags mocks base method. +func (m *MockAPIClient) GetHuntTags(arg0 context.Context, arg1 *emptypb.Empty, arg2 ...grpc.CallOption) (*proto0.HuntTags, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetHuntTags", varargs...) + ret0, _ := ret[0].(*proto0.HuntTags) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHuntTags indicates an expected call of GetHuntTags. +func (mr *MockAPIClientMockRecorder) GetHuntTags(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHuntTags", reflect.TypeOf((*MockAPIClient)(nil).GetHuntTags), varargs...) +} + // GetKeywordCompletions mocks base method. func (m *MockAPIClient) GetKeywordCompletions(arg0 context.Context, arg1 *emptypb.Empty, arg2 ...grpc.CallOption) (*proto0.KeywordCompletions, error) { m.ctrl.T.Helper() @@ -561,6 +661,46 @@ func (mr *MockAPIClientMockRecorder) GetReport(arg0, arg1 interface{}, arg2 ...i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReport", reflect.TypeOf((*MockAPIClient)(nil).GetReport), varargs...) } +// GetSecret mocks base method. +func (m *MockAPIClient) GetSecret(arg0 context.Context, arg1 *proto0.Secret, arg2 ...grpc.CallOption) (*proto0.Secret, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetSecret", varargs...) + ret0, _ := ret[0].(*proto0.Secret) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSecret indicates an expected call of GetSecret. +func (mr *MockAPIClientMockRecorder) GetSecret(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecret", reflect.TypeOf((*MockAPIClient)(nil).GetSecret), varargs...) +} + +// GetSecretDefinitions mocks base method. +func (m *MockAPIClient) GetSecretDefinitions(arg0 context.Context, arg1 *emptypb.Empty, arg2 ...grpc.CallOption) (*proto0.SecretDefinitionList, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetSecretDefinitions", varargs...) + ret0, _ := ret[0].(*proto0.SecretDefinitionList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSecretDefinitions indicates an expected call of GetSecretDefinitions. +func (mr *MockAPIClientMockRecorder) GetSecretDefinitions(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecretDefinitions", reflect.TypeOf((*MockAPIClient)(nil).GetSecretDefinitions), varargs...) +} + // GetServerMonitoringState mocks base method. func (m *MockAPIClient) GetServerMonitoringState(arg0 context.Context, arg1 *emptypb.Empty, arg2 ...grpc.CallOption) (*proto2.ArtifactCollectorArgs, error) { m.ctrl.T.Helper() @@ -641,6 +781,26 @@ func (mr *MockAPIClientMockRecorder) GetToolInfo(arg0, arg1 interface{}, arg2 .. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToolInfo", reflect.TypeOf((*MockAPIClient)(nil).GetToolInfo), varargs...) } +// GetUser mocks base method. +func (m *MockAPIClient) GetUser(arg0 context.Context, arg1 *proto0.UserRequest, arg2 ...grpc.CallOption) (*proto0.VelociraptorUser, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetUser", varargs...) + ret0, _ := ret[0].(*proto0.VelociraptorUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUser indicates an expected call of GetUser. +func (mr *MockAPIClientMockRecorder) GetUser(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockAPIClient)(nil).GetUser), varargs...) +} + // GetUserFavorites mocks base method. func (m *MockAPIClient) GetUserFavorites(arg0 context.Context, arg1 *proto0.Favorite, arg2 ...grpc.CallOption) (*proto0.Favorites, error) { m.ctrl.T.Helper() @@ -661,15 +821,35 @@ func (mr *MockAPIClientMockRecorder) GetUserFavorites(arg0, arg1 interface{}, ar return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserFavorites", reflect.TypeOf((*MockAPIClient)(nil).GetUserFavorites), varargs...) } +// GetUserRoles mocks base method. +func (m *MockAPIClient) GetUserRoles(arg0 context.Context, arg1 *proto0.UserRequest, arg2 ...grpc.CallOption) (*proto0.UserRoles, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetUserRoles", varargs...) + ret0, _ := ret[0].(*proto0.UserRoles) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserRoles indicates an expected call of GetUserRoles. +func (mr *MockAPIClientMockRecorder) GetUserRoles(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserRoles", reflect.TypeOf((*MockAPIClient)(nil).GetUserRoles), varargs...) +} + // GetUserUITraits mocks base method. -func (m *MockAPIClient) GetUserUITraits(arg0 context.Context, arg1 *emptypb.Empty, arg2 ...grpc.CallOption) (*proto0.ApiGrrUser, error) { +func (m *MockAPIClient) GetUserUITraits(arg0 context.Context, arg1 *emptypb.Empty, arg2 ...grpc.CallOption) (*proto0.ApiUser, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "GetUserUITraits", varargs...) - ret0, _ := ret[0].(*proto0.ApiGrrUser) + ret0, _ := ret[0].(*proto0.ApiUser) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -802,7 +982,7 @@ func (mr *MockAPIClientMockRecorder) ListHunts(arg0, arg1 interface{}, arg2 ...i } // LoadArtifactPack mocks base method. -func (m *MockAPIClient) LoadArtifactPack(arg0 context.Context, arg1 *proto0.VFSFileBuffer, arg2 ...grpc.CallOption) (*proto0.LoadArtifactPackResponse, error) { +func (m *MockAPIClient) LoadArtifactPack(arg0 context.Context, arg1 *proto0.LoadArtifactPackRequest, arg2 ...grpc.CallOption) (*proto0.LoadArtifactPackResponse, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { @@ -822,7 +1002,7 @@ func (mr *MockAPIClientMockRecorder) LoadArtifactPack(arg0, arg1 interface{}, ar } // ModifyHunt mocks base method. -func (m *MockAPIClient) ModifyHunt(arg0 context.Context, arg1 *proto0.Hunt, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { +func (m *MockAPIClient) ModifyHunt(arg0 context.Context, arg1 *proto0.HuntMutation, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { @@ -841,6 +1021,26 @@ func (mr *MockAPIClientMockRecorder) ModifyHunt(arg0, arg1 interface{}, arg2 ... return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModifyHunt", reflect.TypeOf((*MockAPIClient)(nil).ModifyHunt), varargs...) } +// ModifySecret mocks base method. +func (m *MockAPIClient) ModifySecret(arg0 context.Context, arg1 *proto0.ModifySecretRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ModifySecret", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ModifySecret indicates an expected call of ModifySecret. +func (mr *MockAPIClientMockRecorder) ModifySecret(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModifySecret", reflect.TypeOf((*MockAPIClient)(nil).ModifySecret), varargs...) +} + // NewNotebook mocks base method. func (m *MockAPIClient) NewNotebook(arg0 context.Context, arg1 *proto0.NotebookMetadata, arg2 ...grpc.CallOption) (*proto0.NotebookMetadata, error) { m.ctrl.T.Helper() @@ -941,15 +1141,155 @@ func (mr *MockAPIClientMockRecorder) Query(arg0, arg1 interface{}, arg2 ...inter return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockAPIClient)(nil).Query), varargs...) } +// ReformatVQL mocks base method. +func (m *MockAPIClient) ReformatVQL(arg0 context.Context, arg1 *proto0.ReformatVQLMessage, arg2 ...grpc.CallOption) (*proto0.ReformatVQLMessage, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ReformatVQL", varargs...) + ret0, _ := ret[0].(*proto0.ReformatVQLMessage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReformatVQL indicates an expected call of ReformatVQL. +func (mr *MockAPIClientMockRecorder) ReformatVQL(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReformatVQL", reflect.TypeOf((*MockAPIClient)(nil).ReformatVQL), varargs...) +} + +// RemoveNotebookAttachment mocks base method. +func (m *MockAPIClient) RemoveNotebookAttachment(arg0 context.Context, arg1 *proto0.NotebookFileUploadRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RemoveNotebookAttachment", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RemoveNotebookAttachment indicates an expected call of RemoveNotebookAttachment. +func (mr *MockAPIClientMockRecorder) RemoveNotebookAttachment(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveNotebookAttachment", reflect.TypeOf((*MockAPIClient)(nil).RemoveNotebookAttachment), varargs...) +} + +// ResumeFlow mocks base method. +func (m *MockAPIClient) ResumeFlow(arg0 context.Context, arg1 *proto0.ApiFlowRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ResumeFlow", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ResumeFlow indicates an expected call of ResumeFlow. +func (mr *MockAPIClientMockRecorder) ResumeFlow(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResumeFlow", reflect.TypeOf((*MockAPIClient)(nil).ResumeFlow), varargs...) +} + +// RevertNotebookCell mocks base method. +func (m *MockAPIClient) RevertNotebookCell(arg0 context.Context, arg1 *proto0.NotebookCellRequest, arg2 ...grpc.CallOption) (*proto0.NotebookCell, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RevertNotebookCell", varargs...) + ret0, _ := ret[0].(*proto0.NotebookCell) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RevertNotebookCell indicates an expected call of RevertNotebookCell. +func (mr *MockAPIClientMockRecorder) RevertNotebookCell(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertNotebookCell", reflect.TypeOf((*MockAPIClient)(nil).RevertNotebookCell), varargs...) +} + +// Scheduler mocks base method. +func (m *MockAPIClient) Scheduler(arg0 context.Context, arg1 ...grpc.CallOption) (proto0.API_SchedulerClient, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Scheduler", varargs...) + ret0, _ := ret[0].(proto0.API_SchedulerClient) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Scheduler indicates an expected call of Scheduler. +func (mr *MockAPIClientMockRecorder) Scheduler(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scheduler", reflect.TypeOf((*MockAPIClient)(nil).Scheduler), varargs...) +} + +// SearchDocs mocks base method. +func (m *MockAPIClient) SearchDocs(arg0 context.Context, arg1 *proto0.DocSearchRequest, arg2 ...grpc.CallOption) (*proto0.DocSearchResponses, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SearchDocs", varargs...) + ret0, _ := ret[0].(*proto0.DocSearchResponses) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchDocs indicates an expected call of SearchDocs. +func (mr *MockAPIClientMockRecorder) SearchDocs(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchDocs", reflect.TypeOf((*MockAPIClient)(nil).SearchDocs), varargs...) +} + +// SearchFile mocks base method. +func (m *MockAPIClient) SearchFile(arg0 context.Context, arg1 *proto0.SearchFileRequest, arg2 ...grpc.CallOption) (*proto0.SearchFileResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SearchFile", varargs...) + ret0, _ := ret[0].(*proto0.SearchFileResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchFile indicates an expected call of SearchFile. +func (mr *MockAPIClientMockRecorder) SearchFile(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchFile", reflect.TypeOf((*MockAPIClient)(nil).SearchFile), varargs...) +} + // SetArtifactFile mocks base method. -func (m *MockAPIClient) SetArtifactFile(arg0 context.Context, arg1 *proto0.SetArtifactRequest, arg2 ...grpc.CallOption) (*proto0.APIResponse, error) { +func (m *MockAPIClient) SetArtifactFile(arg0 context.Context, arg1 *proto0.SetArtifactRequest, arg2 ...grpc.CallOption) (*proto0.SetArtifactResponse, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "SetArtifactFile", varargs...) - ret0, _ := ret[0].(*proto0.APIResponse) + ret0, _ := ret[0].(*proto0.SetArtifactResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -962,7 +1302,7 @@ func (mr *MockAPIClientMockRecorder) SetArtifactFile(arg0, arg1 interface{}, arg } // SetClientMetadata mocks base method. -func (m *MockAPIClient) SetClientMetadata(arg0 context.Context, arg1 *proto0.ClientMetadata, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { +func (m *MockAPIClient) SetClientMetadata(arg0 context.Context, arg1 *proto0.SetClientMetadataRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { @@ -1002,14 +1342,14 @@ func (mr *MockAPIClientMockRecorder) SetClientMonitoringState(arg0, arg1 interfa } // SetGUIOptions mocks base method. -func (m *MockAPIClient) SetGUIOptions(arg0 context.Context, arg1 *proto0.SetGUIOptionsRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { +func (m *MockAPIClient) SetGUIOptions(arg0 context.Context, arg1 *proto0.SetGUIOptionsRequest, arg2 ...grpc.CallOption) (*proto0.SetGUIOptionsResponse, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "SetGUIOptions", varargs...) - ret0, _ := ret[0].(*emptypb.Empty) + ret0, _ := ret[0].(*proto0.SetGUIOptionsResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1021,6 +1361,26 @@ func (mr *MockAPIClientMockRecorder) SetGUIOptions(arg0, arg1 interface{}, arg2 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGUIOptions", reflect.TypeOf((*MockAPIClient)(nil).SetGUIOptions), varargs...) } +// SetPassword mocks base method. +func (m *MockAPIClient) SetPassword(arg0 context.Context, arg1 *proto0.SetPasswordRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SetPassword", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SetPassword indicates an expected call of SetPassword. +func (mr *MockAPIClientMockRecorder) SetPassword(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPassword", reflect.TypeOf((*MockAPIClient)(nil).SetPassword), varargs...) +} + // SetServerMonitoringState mocks base method. func (m *MockAPIClient) SetServerMonitoringState(arg0 context.Context, arg1 *proto2.ArtifactCollectorArgs, arg2 ...grpc.CallOption) (*proto2.ArtifactCollectorArgs, error) { m.ctrl.T.Helper() @@ -1081,6 +1441,26 @@ func (mr *MockAPIClientMockRecorder) SetToolInfo(arg0, arg1 interface{}, arg2 .. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetToolInfo", reflect.TypeOf((*MockAPIClient)(nil).SetToolInfo), varargs...) } +// SetUserRoles mocks base method. +func (m *MockAPIClient) SetUserRoles(arg0 context.Context, arg1 *proto0.UserRoles, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SetUserRoles", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SetUserRoles indicates an expected call of SetUserRoles. +func (mr *MockAPIClientMockRecorder) SetUserRoles(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserRoles", reflect.TypeOf((*MockAPIClient)(nil).SetUserRoles), varargs...) +} + // UpdateNotebook mocks base method. func (m *MockAPIClient) UpdateNotebook(arg0 context.Context, arg1 *proto0.NotebookMetadata, arg2 ...grpc.CallOption) (*proto0.NotebookMetadata, error) { m.ctrl.T.Helper() @@ -1141,6 +1521,26 @@ func (mr *MockAPIClientMockRecorder) UploadNotebookAttachment(arg0, arg1 interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadNotebookAttachment", reflect.TypeOf((*MockAPIClient)(nil).UploadNotebookAttachment), varargs...) } +// VFSDownloadFile mocks base method. +func (m *MockAPIClient) VFSDownloadFile(arg0 context.Context, arg1 *proto0.VFSStatDownloadRequest, arg2 ...grpc.CallOption) (*proto0.StartFlowResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "VFSDownloadFile", varargs...) + ret0, _ := ret[0].(*proto0.StartFlowResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VFSDownloadFile indicates an expected call of VFSDownloadFile. +func (mr *MockAPIClientMockRecorder) VFSDownloadFile(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VFSDownloadFile", reflect.TypeOf((*MockAPIClient)(nil).VFSDownloadFile), varargs...) +} + // VFSGetBuffer mocks base method. func (m *MockAPIClient) VFSGetBuffer(arg0 context.Context, arg1 *proto0.VFSFileBuffer, arg2 ...grpc.CallOption) (*proto0.VFSFileBuffer, error) { m.ctrl.T.Helper() @@ -1181,6 +1581,26 @@ func (mr *MockAPIClientMockRecorder) VFSListDirectory(arg0, arg1 interface{}, ar return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VFSListDirectory", reflect.TypeOf((*MockAPIClient)(nil).VFSListDirectory), varargs...) } +// VFSListDirectoryFiles mocks base method. +func (m *MockAPIClient) VFSListDirectoryFiles(arg0 context.Context, arg1 *proto0.GetTableRequest, arg2 ...grpc.CallOption) (*proto0.GetTableResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "VFSListDirectoryFiles", varargs...) + ret0, _ := ret[0].(*proto0.GetTableResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VFSListDirectoryFiles indicates an expected call of VFSListDirectoryFiles. +func (mr *MockAPIClientMockRecorder) VFSListDirectoryFiles(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VFSListDirectoryFiles", reflect.TypeOf((*MockAPIClient)(nil).VFSListDirectoryFiles), varargs...) +} + // VFSRefreshDirectory mocks base method. func (m *MockAPIClient) VFSRefreshDirectory(arg0 context.Context, arg1 *proto0.VFSRefreshDirectoryRequest, arg2 ...grpc.CallOption) (*proto2.ArtifactCollectorResponse, error) { m.ctrl.T.Helper() diff --git a/api/notebooks.go b/api/notebooks.go index 6daa462d3..37e4dde9b 100644 --- a/api/notebooks.go +++ b/api/notebooks.go @@ -1,44 +1,26 @@ package api import ( - "crypto/rand" - "encoding/base32" - "encoding/base64" - "encoding/binary" - "fmt" + "context" "os" "strings" + "sync" "time" - errors "github.com/pkg/errors" - "github.com/sirupsen/logrus" - context "golang.org/x/net/context" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "github.com/Velocidex/ordereddict" + errors "github.com/go-errors/errors" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/emptypb" "www.velocidex.com/golang/velociraptor/acls" api_proto "www.velocidex.com/golang/velociraptor/api/proto" - artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" - config_proto "www.velocidex.com/golang/velociraptor/config/proto" - "www.velocidex.com/golang/velociraptor/datastore" - file_store "www.velocidex.com/golang/velociraptor/file_store" - "www.velocidex.com/golang/velociraptor/file_store/api" - "www.velocidex.com/golang/velociraptor/flows" - "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/logging" - "www.velocidex.com/golang/velociraptor/paths" - "www.velocidex.com/golang/velociraptor/reporting" "www.velocidex.com/golang/velociraptor/services" - users "www.velocidex.com/golang/velociraptor/users" - vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/server/notebooks" ) -func (self *ApiServer) ExportNotebook( - ctx context.Context, - in *api_proto.NotebookExportRequest) (*emptypb.Empty, error) { - return nil, errors.New("not implementated") -} +const ( + SKIP_UPLOADS = false +) // Get all the current user's notebooks and those notebooks shared // with them. @@ -49,109 +31,87 @@ func (self *ApiServer) GetNotebooks( defer Instrument("GetNotebooks")() // Empty creators are called internally. - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { return nil, err } + principal := user_record.Name permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, - "User is not allowed to read notebooks.") + return nil, PermissionDenied(err, "User is not allowed to read notebooks.") } result := &api_proto.Notebooks{} - db, err := datastore.GetDB(self.config) + notebook_manager, err := services.GetNotebookManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - // We want a single notebook metadata. - if in.NotebookId != "" { - notebook_path_manager := paths.NewNotebookPathManager( - in.NotebookId) - notebook := &api_proto.NotebookMetadata{} - err := db.GetSubject(self.config, notebook_path_manager.Path(), - notebook) - - // Handle the EOF especially: it means there is no such - // notebook and return an empty result set. - if errors.Is(err, os.ErrNotExist) || notebook.NotebookId == "" { - return result, nil - } + // List all the timelines + if in.IncludeTimelines { + // This is only called for global notebooks because client and + // hunt notebooks always specify the exact notebook id. + notebooks, err := notebook_manager.GetAllNotebooks(ctx, + services.NotebookSearchOptions{ + Username: principal, + Timelines: true, + }) if err != nil { - logging.GetLogger( - self.config, &logging.FrontendComponent). - Error("Unable to open notebook: %v", err) - return nil, err + return nil, Status(self.verbose, err) } - // An error here just means there are no AvailableDownloads. - notebook.AvailableDownloads, _ = getAvailableDownloadFiles(self.config, - notebook_path_manager.HtmlExport().Dir()) - - notebook.Timelines = getAvailableTimelines( - self.config, notebook_path_manager) - - result.Items = append(result.Items, notebook) - - // Document not owned or collaborated with. - if !reporting.CheckNotebookAccess(notebook, user_record.Name) { - logging.GetLogger( - self.config, &logging.Audit).WithFields( - logrus.Fields{ - "user": user_record.Name, - "action": "Access Denied", - "notebook": in.NotebookId, - }). - Error("notebook not shared.", err) - return nil, errors.New("User has no access to this notebook") - } + for _, n := range notebooks { + result.Items = append(result.Items, + proto.Clone(n).(*api_proto.NotebookMetadata)) + if uint64(len(result.Items)) > in.Count { + break + } + } return result, nil } - notebooks, err := reporting.GetSharedNotebooks(self.config, user_record.Name, - in.Offset, in.Count) - if err != nil { - return nil, err + if in.NotebookId == "" { + return nil, Status(self.verbose, errors.New("NotebookId must be specified")) } - result.Items = notebooks - return result, nil -} - -func NewNotebookId() string { - buf := make([]byte, 8) - _, _ = rand.Read(buf) - - binary.BigEndian.PutUint32(buf, uint32(time.Now().Unix())) - result := base32.HexEncoding.EncodeToString(buf)[:13] - - return "N." + result -} - -func NewNotebookAttachmentId() string { - buf := make([]byte, 8) - _, _ = rand.Read(buf) - - binary.BigEndian.PutUint32(buf, uint32(time.Now().Unix())) - result := base32.HexEncoding.EncodeToString(buf)[:13] + notebook_metadata, err := notebook_manager.GetNotebook( + ctx, in.NotebookId, in.IncludeUploads) + // Handle the EOF especially: it means there is no such + // notebook and return an empty result set. + if errors.Is(err, os.ErrNotExist) || + (notebook_metadata != nil && notebook_metadata.NotebookId == "") { + return result, nil + } - return "NA." + result -} + if err != nil { + logging.GetLogger( + org_config_obj, &logging.FrontendComponent). + Error("Unable to open notebook: %v", err) + return nil, Status(self.verbose, err) + } -func NewNotebookCellId() string { - buf := make([]byte, 8) - _, _ = rand.Read(buf) + // Document not owned or collaborated with. + if !notebook_manager.CheckNotebookAccess(notebook_metadata, principal) { + err := services.LogAudit(ctx, + org_config_obj, principal, "notebook not shared.", + ordereddict.NewDict(). + Set("action", "Access Denied"). + Set("notebook", in.NotebookId)) + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.FrontendComponent) + logger.Error("notebook not shared %v %v", principal, in.NotebookId) + } - binary.BigEndian.PutUint32(buf, uint32(time.Now().Unix())) - result := base32.HexEncoding.EncodeToString(buf)[:13] + return nil, InvalidStatus("User has no access to this notebook") + } - return "NC." + result + result.Items = append(result.Items, notebook_metadata) + return result, nil } func (self *ApiServer) NewNotebook( @@ -160,311 +120,58 @@ func (self *ApiServer) NewNotebook( defer Instrument("NewNotebook")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } + principal := user_record.Name permissions := acls.NOTEBOOK_EDITOR - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to create notebooks.") } - in.Creator = user_name - in.CreatedTime = time.Now().Unix() - in.ModifiedTime = in.CreatedTime - - // Allow hunt notebooks to be created with a specified hunt ID. - if !strings.HasPrefix(in.NotebookId, "N.H.") && - !strings.HasPrefix(in.NotebookId, "N.F.") { - in.NotebookId = NewNotebookId() - } - - db, err := datastore.GetDB(self.config) - if err != nil { - return nil, err - } - - // Store the notebook metadata first before creating the - // cells. Calculating the cells will try to open the notebook. - notebook_path_manager := paths.NewNotebookPathManager(in.NotebookId) - err = db.SetSubject(self.config, notebook_path_manager.Path(), in) + notebook_manager, err := services.GetNotebookManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - err = self.createInitialNotebook(ctx, user_name, in) - if err != nil { - return nil, err - } - - // Add the new notebook to the index so it can be seen. Only - // non-hunt notebooks are searchable in the index since the - // hunt notebooks are always found in the hunt results. - err = reporting.UpdateShareIndex(self.config, in) - if err != nil { - return nil, err - } - - err = db.SetSubject(self.config, notebook_path_manager.Path(), in) - return in, err -} - -// Create the initial cells of the notebook. -func (self *ApiServer) createInitialNotebook( - ctx context.Context, - user_name string, - notebook_metadata *api_proto.NotebookMetadata) error { - - // All cells receive a header from the name and description of - // the notebook. - new_cells := []*api_proto.NotebookCellRequest{{ - Input: fmt.Sprintf("# %s\n\n%s\n", notebook_metadata.Name, - notebook_metadata.Description), - Type: "Markdown", - CurrentlyEditing: true, - }} - - if notebook_metadata.Context != nil { - if notebook_metadata.Context.HuntId != "" { - new_cells = getCellsForHunt(ctx, self.config, - notebook_metadata.Context.HuntId, notebook_metadata) - } else if notebook_metadata.Context.FlowId != "" && - notebook_metadata.Context.ClientId != "" { - new_cells = getCellsForFlow(ctx, self.config, - notebook_metadata.Context.ClientId, - notebook_metadata.Context.FlowId, notebook_metadata) - } - } - - for _, cell := range new_cells { - new_cell_id := NewNotebookCellId() - - notebook_metadata.CellMetadata = append(notebook_metadata.CellMetadata, &api_proto.NotebookCell{ - CellId: new_cell_id, - Env: cell.Env, - Timestamp: time.Now().Unix(), - }) - cell.NotebookId = notebook_metadata.NotebookId - cell.CellId = new_cell_id - - _, err := self.updateNotebookCell(ctx, notebook_metadata, user_name, cell) - if err != nil { - return err - } - } - return nil -} - -func getCellsForHunt(ctx context.Context, - config_obj *config_proto.Config, - hunt_id string, - notebook_metadata *api_proto.NotebookMetadata) []*api_proto.NotebookCellRequest { - - dispatcher := services.GetHuntDispatcher() - if dispatcher == nil { - return nil - } - - hunt_obj, pres := dispatcher.GetHunt(hunt_id) - if !pres { - return nil - } - sources := hunt_obj.ArtifactSources - if len(sources) == 0 { - if hunt_obj.StartRequest != nil { - sources = hunt_obj.StartRequest.Artifacts - } else { - return nil - } - } - - return getDefaultCellsForSources(config_obj, sources, notebook_metadata) -} - -func getCellsForFlow(ctx context.Context, - config_obj *config_proto.Config, - client_id, flow_id string, - notebook_metadata *api_proto.NotebookMetadata) []*api_proto.NotebookCellRequest { - - flow_context, err := flows.LoadCollectionContext(config_obj, client_id, flow_id) - if err != nil { - return nil - } - - sources := flow_context.ArtifactsWithResults - if len(sources) == 0 && flow_context.Request != nil { - sources = flow_context.Request.Artifacts - } - - return getDefaultCellsForSources(config_obj, sources, notebook_metadata) -} - -func getDefaultCellsForSources( - config_obj *config_proto.Config, - sources []string, - notebook_metadata *api_proto.NotebookMetadata) []*api_proto.NotebookCellRequest { - manager, err := services.GetRepositoryManager() - if err != nil { - return nil - } - - repository, err := manager.GetGlobalRepository(config_obj) - if err != nil { - return nil - } - - // Create one table per artifact by default. - var result []*api_proto.NotebookCellRequest - - for _, source := range sources { - artifact, pres := repository.Get(config_obj, source) - if pres { - notebook_metadata.ColumnTypes = append(notebook_metadata.ColumnTypes, - artifact.ColumnTypes...) - } - - // Check if the artifact has custom notebook cells defined. - artifact_source, pres := repository.GetSource(config_obj, source) - if !pres { - continue - } - env := []*api_proto.Env{{ - Key: "ArtifactName", Value: source, - }} - - // If the artifact_source defines a notebook, let it do its own thing. - if len(artifact_source.Notebook) > 0 { - for _, cell := range artifact_source.Notebook { - for _, i := range cell.Env { - env = append(env, &api_proto.Env{ - Key: i.Key, - Value: i.Value, - }) - } - - result = append(result, &api_proto.NotebookCellRequest{ - Type: cell.Type, - Env: env, - Input: cell.Template}) - } - - } else { - // Otherwise build a default notebook. - result = append(result, &api_proto.NotebookCellRequest{ - Type: "Markdown", - Env: env, - Input: "# " + source}) - - result = append(result, &api_proto.NotebookCellRequest{ - Type: "VQL", - Env: env, - Input: "\nSELECT * FROM source()\nLIMIT 50\n", - }) - } - } - - return result + return notebook_manager.NewNotebook(ctx, principal, in) } func (self *ApiServer) NewNotebookCell( ctx context.Context, - in *api_proto.NotebookCellRequest) (*api_proto.NotebookMetadata, error) { + in *api_proto.NotebookCellRequest) ( + *api_proto.NotebookMetadata, error) { defer Instrument("NewNotebookCell")() if !strings.HasPrefix(in.NotebookId, "N.") { - return nil, errors.New("Invalid NoteboookId") + return nil, InvalidStatus("Invalid NoteboookId") } - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } + principal := user_record.Name permissions := acls.NOTEBOOK_EDITOR - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to edit notebooks.") } - db, err := datastore.GetDB(self.config) - if err != nil { - return nil, err - } - - // Set a default artifact. - if in.Input == "" && in.Type == "Artifact" { - in.Input = default_artifact - } - - notebook := &api_proto.NotebookMetadata{} - notebook_path_manager := paths.NewNotebookPathManager(in.NotebookId) - err = db.GetSubject(self.config, notebook_path_manager.Path(), notebook) - if err != nil { - return nil, err - } - - new_cell_md := []*api_proto.NotebookCell{} - added := false - - notebook.LatestCellId = NewNotebookCellId() - - for _, cell_md := range notebook.CellMetadata { - if cell_md.CellId == in.CellId { - new_cell_md = append(new_cell_md, &api_proto.NotebookCell{ - CellId: cell_md.CellId, - Timestamp: time.Now().Unix(), - }) - new_cell_md = append(new_cell_md, &api_proto.NotebookCell{ - CellId: notebook.LatestCellId, - Timestamp: time.Now().Unix(), - }) - added = true - continue - } - new_cell_md = append(new_cell_md, cell_md) - } - - // Add it to the end of the document. - if !added { - new_cell_md = append(new_cell_md, &api_proto.NotebookCell{ - CellId: notebook.LatestCellId, - Timestamp: time.Now().Unix(), - }) - } - - notebook.CellMetadata = new_cell_md - - err = db.SetSubject(self.config, notebook_path_manager.Path(), notebook) + notebook_manager, err := services.GetNotebookManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - - // Start off with some empty lines. - if in.Input == "" { - in.Input = "\n\n\n\n\n\n" - } - - // Create the new cell with fresh content. - new_cell_request := &api_proto.NotebookCellRequest{ - Input: in.Input, - NotebookId: in.NotebookId, - CellId: notebook.LatestCellId, - Type: in.Type, - Env: in.Env, - - // New cells are opened for editing. - CurrentlyEditing: true, - } - - _, err = self.UpdateNotebookCell(ctx, new_cell_request) - return notebook, err + return notebook_manager.NewNotebookCell(ctx, in, principal) } func (self *ApiServer) UpdateNotebook( @@ -474,42 +181,37 @@ func (self *ApiServer) UpdateNotebook( defer Instrument("UpdateNotebook")() if !strings.HasPrefix(in.NotebookId, "N.") { - return nil, errors.New("Invalid NoteboookId") + return nil, InvalidStatus("Invalid NoteboookId") } - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } + principal := user_record.Name permissions := acls.NOTEBOOK_EDITOR - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to edit notebooks.") } - db, err := datastore.GetDB(self.config) + // If the notebook is not properly shared with the user they + // may not edit it. + notebook_manager, err := services.GetNotebookManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - old_notebook := &api_proto.NotebookMetadata{} - notebook_path_manager := paths.NewNotebookPathManager(in.NotebookId) - err = db.GetSubject(self.config, notebook_path_manager.Path(), old_notebook) + old_notebook, err := notebook_manager.GetNotebook(ctx, in.NotebookId, SKIP_UPLOADS) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - // If the notebook is not properly shared with the user they - // may not edit it. - if !reporting.CheckNotebookAccess(old_notebook, user_record.Name) { - return nil, errors.New("Notebook is not shared with user.") - } - - if old_notebook.ModifiedTime != in.ModifiedTime { - return nil, errors.New("Edit clash detected.") + if !notebook_manager.CheckNotebookAccess(old_notebook, principal) { + return nil, InvalidStatus("Notebook is not shared with user.") } // When updating an existing notebook only certain fields may @@ -529,14 +231,53 @@ func (self *ApiServer) UpdateNotebook( } in.CellMetadata = cell_metadata - err = db.SetSubject(self.config, notebook_path_manager.Path(), in) + return in, notebook_manager.UpdateNotebook(ctx, in) +} + +func (self *ApiServer) DeleteNotebook( + ctx context.Context, + in *api_proto.NotebookMetadata) (*emptypb.Empty, error) { + + defer Instrument("DeleteNotebook")() + + if !strings.HasPrefix(in.NotebookId, "N.") { + return nil, InvalidStatus("Invalid NoteboookId") + } + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + permissions := acls.NOTEBOOK_EDITOR + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to delete notebooks.") + } + + // If the notebook is not properly shared with the user they + // may not edit it. + notebook_manager, err := services.GetNotebookManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + old_notebook, err := notebook_manager.GetNotebook(ctx, in.NotebookId, SKIP_UPLOADS) + if err != nil { + return nil, Status(self.verbose, err) + } + + if !notebook_manager.CheckNotebookAccess(old_notebook, principal) { + return nil, InvalidStatus("Notebook is not shared with user.") } - // Now also update the indexes. - err = reporting.UpdateShareIndex(self.config, in) - return in, err + err = notebook_manager.DeleteNotebook(ctx, in.NotebookId, nil, + true /* really_do_it */) + + return &emptypb.Empty{}, Status(self.verbose, err) } func (self *ApiServer) GetNotebookCell( @@ -546,65 +287,42 @@ func (self *ApiServer) GetNotebookCell( defer Instrument("GetNotebookCell")() if !strings.HasPrefix(in.NotebookId, "N.") { - return nil, errors.New("Invalid NoteboookId") + return nil, InvalidStatus("Invalid NotebookId") } if !strings.HasPrefix(in.CellId, "NC.") { - return nil, errors.New("Invalid NoteboookCellId") + return nil, InvalidStatus("Invalid NotebookCellId") } - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } + principal := user_record.Name permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to read notebooks.") } - db, err := datastore.GetDB(self.config) + notebook_manager, err := services.GetNotebookManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - // Check the user is allowed to manipulate this notebook. - notebook_path_manager := paths.NewNotebookPathManager(in.NotebookId) - - notebook_metadata := &api_proto.NotebookMetadata{} - err = db.GetSubject(self.config, - notebook_path_manager.Path(), notebook_metadata) + notebook_metadata, err := notebook_manager.GetNotebook(ctx, in.NotebookId, SKIP_UPLOADS) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - if !reporting.CheckNotebookAccess(notebook_metadata, user_record.Name) { - return nil, errors.New("Notebook is not shared with user.") + if !notebook_manager.CheckNotebookAccess(notebook_metadata, principal) { + return nil, InvalidStatus("Notebook is not shared with user.") } - notebook := &api_proto.NotebookCell{} - err = db.GetSubject(self.config, - notebook_path_manager.Cell(in.CellId).Path(), - notebook) - - // Cell does not exist, make it a default cell. - if errors.Is(err, os.ErrNotExist) { - return &api_proto.NotebookCell{ - Input: "", - Output: "", - Data: "{}", - CellId: in.CellId, - Type: "Markdown", - }, nil - } - if err != nil { - return nil, err - } - - return notebook, nil + return notebook_manager.GetNotebookCell(ctx, in.NotebookId, in.CellId, in.Version) } func (self *ApiServer) UpdateNotebookCell( @@ -614,200 +332,93 @@ func (self *ApiServer) UpdateNotebookCell( defer Instrument("UpdateNotebookCell")() if !strings.HasPrefix(in.NotebookId, "N.") { - return nil, errors.New("Invalid NoteboookId") + return nil, InvalidStatus("Invalid NotebookId") } if !strings.HasPrefix(in.CellId, "NC.") { - return nil, errors.New("Invalid NoteboookCellId") + return nil, InvalidStatus("Invalid NotebookCellId") } - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } + principal := user_record.Name permissions := acls.NOTEBOOK_EDITOR - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to edit notebooks.") } - // Check that the user has access to this notebook. - notebook_path_manager := paths.NewNotebookPathManager(in.NotebookId) - notebook_metadata := &api_proto.NotebookMetadata{} - db, err := datastore.GetDB(self.config) + notebook_manager, err := services.GetNotebookManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - err = db.GetSubject(self.config, - notebook_path_manager.Path(), notebook_metadata) + // Check that the user has access to this notebook. + notebook_metadata, err := notebook_manager.GetNotebook(ctx, in.NotebookId, SKIP_UPLOADS) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - if !reporting.CheckNotebookAccess(notebook_metadata, user_record.Name) { - return nil, errors.New("Notebook is not shared with user.") + if !notebook_manager.CheckNotebookAccess(notebook_metadata, principal) { + return nil, InvalidStatus("Notebook is not shared with user.") } - return self.updateNotebookCell(ctx, notebook_metadata, user_name, in) + res, err := notebook_manager.UpdateNotebookCell( + ctx, notebook_metadata, principal, in) + return res, Status(self.verbose, err) } -func (self *ApiServer) updateNotebookCell( +func (self *ApiServer) RevertNotebookCell( ctx context.Context, - notebook_metadata *api_proto.NotebookMetadata, - user_name string, in *api_proto.NotebookCellRequest) (*api_proto.NotebookCell, error) { - notebook_cell := &api_proto.NotebookCell{ - Input: in.Input, - Output: `
Calculating...
`, - CellId: in.CellId, - Type: in.Type, - Timestamp: time.Now().Unix(), - CurrentlyEditing: in.CurrentlyEditing, - Calculating: true, - Env: in.Env, - } + defer Instrument("RevertNotebookCell")() - db, err := datastore.GetDB(self.config) - if err != nil { - return nil, err + if !strings.HasPrefix(in.NotebookId, "N.") { + return nil, InvalidStatus("Invalid NotebookId") } - // And store it for next time. - notebook_path_manager := paths.NewNotebookPathManager( - notebook_metadata.NotebookId) - err = db.SetSubject(self.config, - notebook_path_manager.Cell(in.CellId).Path(), - notebook_cell) - if err != nil { - return nil, err + if !strings.HasPrefix(in.CellId, "NC.") { + return nil, InvalidStatus("Invalid NotebookCellId") } - // Run the actual query independently. - query_ctx, query_cancel := context.WithCancel(context.Background()) - - acl_manager := vql_subsystem.NewServerACLManager(self.config, user_name) - - manager, err := services.GetRepositoryManager() - if err != nil { - return nil, err - } - global_repo, err := manager.GetGlobalRepository(self.config) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } + principal := user_record.Name - tmpl, err := reporting.NewGuiTemplateEngine( - self.config, query_ctx, nil, acl_manager, global_repo, - notebook_path_manager.Cell(in.CellId), - "Server.Internal.ArtifactDescription") - if err != nil { - return nil, err + permissions := acls.NOTEBOOK_EDITOR + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to edit notebooks.") } - tmpl.SetEnv("NotebookId", in.NotebookId) - - // Register a progress reporter so we can monitor how the - // template rendering is going. - tmpl.Progress = &progressReporter{ - config_obj: self.config, - notebook_cell: notebook_cell, - notebook_id: in.NotebookId, - start: time.Now(), + notebook_manager, err := services.GetNotebookManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) } - // Add the notebook environment into the cell template. - for _, env := range notebook_metadata.Env { - tmpl.SetEnv(env.Key, env.Value) + // Check that the user has access to this notebook. + notebook_metadata, err := notebook_manager.GetNotebook(ctx, in.NotebookId, SKIP_UPLOADS) + if err != nil { + return nil, Status(self.verbose, err) } - // Also apply the cell env - for _, env := range in.Env { - tmpl.SetEnv(env.Key, env.Value) + if !notebook_manager.CheckNotebookAccess(notebook_metadata, principal) { + return nil, InvalidStatus("Notebook is not shared with user.") } - input := in.Input - cell_type := in.Type - - // Update the content asynchronously - start_time := time.Now() - - // RPC call deadline - if we can complete within 1 second pass - // the response directly to the RPC caller. - sub_ctx, sub_cancel := context.WithTimeout(ctx, time.Second) - - // Main error will be delivered to the RPC caller if we can - // complete the entire operation before the deadline. - var main_err error - - // Watcher thread: Wait for cancellation from the GUI or a 10 min timeout. - go func() { - defer query_cancel() - - cancel_notify, remove_notification := services.GetNotifier(). - ListenForNotification(in.CellId) - defer remove_notification() - - default_notebook_expiry := self.config.Defaults.NotebookCellTimeoutMin - if default_notebook_expiry == 0 { - default_notebook_expiry = 10 - } - - select { - // Query is done - get out of here. - case <-query_ctx.Done(): - - // Active cancellation from the GUI. - case <-cancel_notify: - tmpl.Scope.Log("Cancelled after %v !", time.Since(start_time)) - - // Set a timeout. - case <-time.After(time.Duration(default_notebook_expiry) * time.Minute): - tmpl.Scope.Log("Query timed out after %v !", time.Since(start_time)) - } - - }() - - // Main worker: Just run the query until done. - go func() { - // Cancel and release the main thread if we - // finish quickly before the timeout. - defer sub_cancel() - - // Make sure to cancel the query context if we - // finished early - the Waiter goroutine above will be - // released. - defer query_cancel() - - // Close the template when we are done with it. - defer tmpl.Close() - - resp, err := updateCellContents(query_ctx, self.config, tmpl, - in.CurrentlyEditing, in.NotebookId, - in.CellId, cell_type, in.Env, input, in.Input) - if err != nil { - main_err = err - logger := logging.GetLogger(self.config, &logging.GUIComponent) - logger.Error("Rendering error: %v", err) - } - - // Update the response if we can. - if resp != nil { - notebook_cell = resp - } - }() - - // Wait here up to 1 second for immediate response - but if - // the response takes too long, just give up and return a - // continuation. The GUI will continue polling for notebook - // state and will pick up the changes by itself. - <-sub_ctx.Done() - - return notebook_cell, main_err + res, err := notebook_manager.RevertNotebookCellVersion( + ctx, in.NotebookId, in.CellId, in.Version) + return res, Status(self.verbose, err) } func (self *ApiServer) CancelNotebookCell( @@ -817,51 +428,34 @@ func (self *ApiServer) CancelNotebookCell( defer Instrument("CancelNotebookCell")() if !strings.HasPrefix(in.NotebookId, "N.") { - return nil, errors.New("Invalid NoteboookId") + return nil, InvalidStatus("Invalid NotebookId") } if !strings.HasPrefix(in.CellId, "NC.") { - return nil, errors.New("Invalid NoteboookCellId") + return nil, InvalidStatus("Invalid NotebookCellId") } - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } + principal := user_record.Name permissions := acls.NOTEBOOK_EDITOR - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to edit notebooks.") } - // Unset the calculating bit in the notebook in case the - // renderer is not actually running (e.g. server restart). - db, err := datastore.GetDB(self.config) + notebook_manager, err := services.GetNotebookManager(org_config_obj) if err != nil { - return nil, err - } - notebook_cell_path_manager := paths.NewNotebookPathManager( - in.NotebookId).Cell(in.CellId) - notebook_cell := &api_proto.NotebookCell{} - err = db.GetSubject(self.config, notebook_cell_path_manager.Path(), - notebook_cell) - if err != nil || notebook_cell.CellId != in.CellId { - return nil, errors.New("No such cell") + return nil, Status(self.verbose, err) } - notebook_cell.Calculating = false - // Make sure we write the cancel message ASAP - err = db.SetSubject(self.config, notebook_cell_path_manager.Path(), - notebook_cell) - if err != nil { - return nil, err - } - - return &emptypb.Empty{}, services.GetNotifier().NotifyListener( - self.config, in.CellId, "CancelNotebookCell") + return &emptypb.Empty{}, notebook_manager.CancelNotebookCell( + ctx, in.NotebookId, in.CellId, in.Version) } func (self *ApiServer) UploadNotebookAttachment( @@ -870,43 +464,29 @@ func (self *ApiServer) UploadNotebookAttachment( defer Instrument("UploadNotebookAttachment")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } + principal := user_record.Name permissions := acls.NOTEBOOK_EDITOR - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to edit notebooks.") } - decoded, err := base64.StdEncoding.DecodeString(in.Data) + notebook_manager, err := services.GetNotebookManager(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - - filename := NewNotebookAttachmentId() + in.Filename - full_path := paths.NewNotebookPathManager(in.NotebookId). - Attachment(filename) - file_store_factory := file_store.GetFileStore(self.config) - fd, err := file_store_factory.WriteFile(full_path) - if err != nil { - return nil, err - } - defer fd.Close() - - _, err = fd.Write(decoded) + res, err := notebook_manager.UploadNotebookAttachment(ctx, in) if err != nil { - return nil, err - } - - result := &api_proto.NotebookFileUploadResponse{ - Url: full_path.AsClientPath(), + return nil, Status(self.verbose, err) } - return result, nil + return res, nil } func (self *ApiServer) CreateNotebookDownloadFile( @@ -915,390 +495,72 @@ func (self *ApiServer) CreateNotebookDownloadFile( defer Instrument("CreateNotebookDownloadFile")() - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } + principal := user_record.Name permissions := acls.PREPARE_RESULTS - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, principal, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to export notebooks.") } + wg := &sync.WaitGroup{} + switch in.Type { case "zip": - return &emptypb.Empty{}, exportZipNotebook( - self.config, in.NotebookId, user_record.Name) - default: - return &emptypb.Empty{}, exportHTMLNotebook( - self.config, in.NotebookId, user_record.Name) - } -} + _, err := notebooks.ExportNotebookToZip(ctx, + org_config_obj, wg, in.NotebookId, + principal, in.PreferredName) -// Create a portable notebook into a zip file. -func exportZipNotebook( - config_obj *config_proto.Config, - notebook_id, principal string) error { - db, err := datastore.GetDB(config_obj) - if err != nil { - return err - } - - notebook := &api_proto.NotebookMetadata{} - notebook_path_manager := paths.NewNotebookPathManager(notebook_id) - err = db.GetSubject(config_obj, notebook_path_manager.Path(), notebook) - if err != nil { - return err - } - - if !reporting.CheckNotebookAccess(notebook, principal) { - return errors.New("Notebook is not shared with user.") - } - - file_store_factory := file_store.GetFileStore(config_obj) - filename := notebook_path_manager.ZipExport() - lock_file_name := filename.SetType(api.PATH_TYPE_FILESTORE_LOCK) - - lock_file, err := file_store_factory.WriteFile(lock_file_name) - if err != nil { - return err - } - lock_file.Close() - - // Allow 1 hour to export the notebook. - sub_ctx, cancel := context.WithTimeout(context.Background(), time.Hour) - - go func() { - defer func() { - _ = file_store_factory.Delete(lock_file_name) - }() - - defer cancel() - - err := reporting.ExportNotebookToZip( - sub_ctx, config_obj, notebook_path_manager) - if err != nil { - logger := logging.GetLogger(config_obj, &logging.GUIComponent) - logger.WithFields(logrus.Fields{ - "notebook_id": notebook.NotebookId, - "export_file": filename, - "error": err, - }).Error("CreateNotebookDownloadFile") - return - } - }() + return &emptypb.Empty{}, Status(self.verbose, err) - return nil -} - -func exportHTMLNotebook(config_obj *config_proto.Config, - notebook_id, principal string) error { - db, err := datastore.GetDB(config_obj) - if err != nil { - return err - } - - notebook := &api_proto.NotebookMetadata{} - notebook_path_manager := paths.NewNotebookPathManager(notebook_id) - err = db.GetSubject(config_obj, notebook_path_manager.Path(), notebook) - if err != nil { - return err - } - - if !reporting.CheckNotebookAccess(notebook, principal) { - return errors.New("Notebook is not shared with user.") - } - - file_store_factory := file_store.GetFileStore(config_obj) - filename := notebook_path_manager.HtmlExport() - lock_file_name := filename.SetType(api.PATH_TYPE_FILESTORE_LOCK) - - lock_file, err := file_store_factory.WriteFile(lock_file_name) - if err != nil { - return err - } - lock_file.Close() - - writer, err := file_store_factory.WriteFile(filename) - if err != nil { - return err - } - - // Allow 1 hour to export the notebook. - sub_ctx, cancel := context.WithTimeout(context.Background(), time.Hour) - - go func() { - defer func() { _ = file_store_factory.Delete(lock_file_name) }() - defer writer.Close() - defer cancel() - - err := reporting.ExportNotebookToHTML( - sub_ctx, config_obj, notebook.NotebookId, writer) - if err != nil { - logger := logging.GetLogger(config_obj, &logging.GUIComponent) - logger.WithFields(logrus.Fields{ - "notebook_id": notebook.NotebookId, - "export_file": filename, - "error": err, - }).Error("CreateNotebookDownloadFile") - return - } - }() - - return nil -} - -func getAvailableTimelines( - config_obj *config_proto.Config, - path_manager *paths.NotebookPathManager) []string { - - result := []string{} - db, err := datastore.GetDB(config_obj) - files, err := db.ListChildren(config_obj, path_manager.SuperTimelineDir()) - if err != nil { - return nil - } - - for _, f := range files { - if !f.IsDir() { - result = append(result, f.Base()) - } - } - return result -} - -func getAvailableDownloadFiles(config_obj *config_proto.Config, - download_path api.FSPathSpec) (*api_proto.AvailableDownloads, error) { - result := &api_proto.AvailableDownloads{} - - file_store_factory := file_store.GetFileStore(config_obj) - files, err := file_store_factory.ListDirectory(download_path) - if err != nil { - return nil, err - } - - is_complete := func(name string) bool { - for _, item := range files { - ps := item.PathSpec() - // If there is a lock file we are not done. - if ps.Base() == name && - ps.Type() == api.PATH_TYPE_FILESTORE_LOCK { - return false - } - } - return true - } - - for _, item := range files { - ps := item.PathSpec() - - // Skip lock files - if ps.Type() == api.PATH_TYPE_FILESTORE_LOCK { - continue - } - - result.Files = append(result.Files, &api_proto.AvailableDownloadFile{ - Name: item.Name(), - Type: api.GetExtensionForFilestore(ps), - Path: ps.AsClientPath(), - Size: uint64(item.Size()), - Date: fmt.Sprintf("%v", item.ModTime()), - Complete: is_complete(ps.Base()), - }) + default: + _, err := notebooks.ExportNotebookToHTML( + org_config_obj, wg, in.NotebookId, + principal, in.PreferredName) + return &emptypb.Empty{}, Status(self.verbose, err) } - - return result, nil } -func updateCellContents( +func (self *ApiServer) RemoveNotebookAttachment( ctx context.Context, - config_obj *config_proto.Config, - tmpl *reporting.GuiTemplateEngine, - currently_editing bool, - notebook_id, cell_id, cell_type string, - env []*api_proto.Env, - input, original_input string) (res *api_proto.NotebookCell, err error) { - - output := "" - - cell_type = strings.ToLower(cell_type) - - // Create a new cell to set the result in. - make_cell := func(output string) *api_proto.NotebookCell { - messages := tmpl.Messages() - - encoded_data, err := json.Marshal(tmpl.Data) - if err != nil { - messages = append(messages, - fmt.Sprintf("Error: %v", err)) - } - - return &api_proto.NotebookCell{ - Input: original_input, - Output: output, - Data: string(encoded_data), - Messages: tmpl.Messages(), - CellId: cell_id, - Type: cell_type, - Env: env, - Timestamp: time.Now().Unix(), - CurrentlyEditing: currently_editing, - Duration: int64(time.Since(tmpl.Start).Seconds()), - } - } - - // If an error occurs it is important to ensure the cell is - // still written with an error message. - make_error_cell := func(output string, err error) ( - *api_proto.NotebookCell, error) { - notebook_cell := make_cell(output) - notebook_cell.Messages = append(notebook_cell.Messages, - fmt.Sprintf("Error: %v", err)) - setCell(config_obj, notebook_id, notebook_cell) - return notebook_cell, err - } - - // Do not let exceptions take down the server. - defer func() { - r := recover() - if r != nil { - res, err = make_error_cell("", fmt.Errorf("PANIC: %v", r)) - } - }() - - switch cell_type { - - case "markdown", "md": - // A Markdown cell just feeds directly into the - // template. - output, err = tmpl.Execute(&artifacts_proto.Report{Template: input}) - if err != nil { - return make_error_cell(output, err) - } - - case "vql": - // A VQL cell gets converted to a set of VQL and - // markdown fragments. - cell_content, err := reporting.ConvertVQLCellToContent(input) - if err != nil { - // Ignore errors and just treat the whole - // thing as VQL - this will fail to render the - // comment and just ignore it - it is probably - // malformed. - cell_content = &reporting.Content{} - cell_content.PushVQL(input) - } + in *api_proto.NotebookFileUploadRequest) (*emptypb.Empty, error) { - for _, fragment := range cell_content.Fragments { - if fragment.VQL != "" { - rows := tmpl.Query(fragment.VQL) - output_any, ok := tmpl.Table(rows).(string) - if ok { - output += output_any - } - - } else if fragment.Comment != "" { - lines := strings.SplitN(fragment.Comment, "\n", 2) - if len(lines) <= 1 { - input = lines[0] - } else { - input = lines[1] - } - fragment_output, err := tmpl.Execute(&artifacts_proto.Report{Template: input}) - if err != nil { - return make_error_cell(output, err) - } - output += fragment_output - } - } + defer Instrument("RemoveNotebookAttachment")() - default: - return make_error_cell(output, errors.New("Unsupported cell type.")) + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) } + principal := user_record.Name - tmpl.Close() - - notebook_cell := make_cell(output) - return notebook_cell, setCell(config_obj, notebook_id, notebook_cell) -} - -func setCell( - config_obj *config_proto.Config, - notebook_id string, - notebook_cell *api_proto.NotebookCell) error { - - db, err := datastore.GetDB(config_obj) - if err != nil { - return err + permissions := acls.PREPARE_RESULTS + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to update notebooks.") } - // And store it for next time. - notebook_path_manager := paths.NewNotebookPathManager(notebook_id) - err = db.SetSubject(config_obj, - notebook_path_manager.Cell(notebook_cell.CellId).Path(), - notebook_cell) + notebook_manager, err := services.GetNotebookManager(org_config_obj) if err != nil { - return err + return nil, Status(self.verbose, err) } - // Open the notebook and update the cell's timestamp. - notebook := &api_proto.NotebookMetadata{} - err = db.GetSubject(config_obj, notebook_path_manager.Path(), notebook) + notebook, err := notebook_manager.GetNotebook(ctx, in.NotebookId, SKIP_UPLOADS) if err != nil { - return err + return nil, Status(self.verbose, err) } - // Update the cell's timestamp so the gui will refresh it. - new_cell_md := []*api_proto.NotebookCell{} - for _, cell_md := range notebook.CellMetadata { - if cell_md.CellId == notebook_cell.CellId { - new_cell_md = append(new_cell_md, &api_proto.NotebookCell{ - CellId: notebook_cell.CellId, - Timestamp: time.Now().Unix(), - }) - continue - } - new_cell_md = append(new_cell_md, cell_md) + if !notebook_manager.CheckNotebookAccess(notebook, principal) { + return nil, InvalidStatus("Notebook is not shared with user.") } - notebook.CellMetadata = new_cell_md - - return db.SetSubject(config_obj, notebook_path_manager.Path(), notebook) -} - -type progressReporter struct { - config_obj *config_proto.Config - notebook_cell *api_proto.NotebookCell - notebook_id, table_id string - last, start time.Time -} -func (self *progressReporter) Report(message string) { - now := time.Now() - if now.Before(self.last.Add(4 * time.Second)) { - return - } - - self.last = now - duration := time.Since(self.start).Round(time.Second) - - notebook_cell := proto.Clone(self.notebook_cell).(*api_proto.NotebookCell) - notebook_cell.Output = fmt.Sprintf(` -
- Calculating... (%v after %v) -
-
- -
-`, - message, duration, - self.notebook_id, self.notebook_cell.CellId, message) - notebook_cell.Timestamp = now.Unix() - notebook_cell.Duration = int64(duration.Seconds()) - - // Cant do anything if we can not set the notebook times - _ = setCell(self.config_obj, self.notebook_id, notebook_cell) + return &emptypb.Empty{}, notebook_manager.RemoveNotebookAttachment(ctx, + in.NotebookId, in.Components) } diff --git a/api/proto/api.pb.go b/api/proto/api.pb.go index b659e8bfc..131f17dfa 100644 --- a/api/proto/api.pb.go +++ b/api/proto/api.pb.go @@ -1,16 +1,13 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: api.proto package proto import ( - empty "github.com/golang/protobuf/ptypes/empty" _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" reflect "reflect" sync "sync" proto2 "www.velocidex.com/golang/velociraptor/actions/proto" @@ -172,9 +169,7 @@ type VFSRefreshDirectoryRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` - // Deprecated - XXXXvfsPath string `protobuf:"bytes,2,opt,name=XXXXvfs_path,json=XXXXvfsPath,proto3" json:"XXXXvfs_path,omitempty"` + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` VfsComponents []string `protobuf:"bytes,4,rep,name=vfs_components,json=vfsComponents,proto3" json:"vfs_components,omitempty"` Depth uint64 `protobuf:"varint,3,opt,name=depth,proto3" json:"depth,omitempty"` } @@ -218,13 +213,6 @@ func (x *VFSRefreshDirectoryRequest) GetClientId() string { return "" } -func (x *VFSRefreshDirectoryRequest) GetXXXXvfsPath() string { - if x != nil { - return x.XXXXvfsPath - } - return "" -} - func (x *VFSRefreshDirectoryRequest) GetVfsComponents() []string { if x != nil { return x.VfsComponents @@ -249,6 +237,8 @@ type VFSFileBuffer struct { Length uint32 `protobuf:"varint,4,opt,name=length,proto3" json:"length,omitempty"` Data []byte `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"` Components []string `protobuf:"bytes,6,rep,name=components,proto3" json:"components,omitempty"` + OrgId string `protobuf:"bytes,7,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + Padding *bool `protobuf:"varint,8,opt,name=padding,proto3,oneof" json:"padding,omitempty"` } func (x *VFSFileBuffer) Reset() { @@ -318,6 +308,20 @@ func (x *VFSFileBuffer) GetComponents() []string { return nil } +func (x *VFSFileBuffer) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *VFSFileBuffer) GetPadding() bool { + if x != nil && x.Padding != nil { + return *x.Padding + } + return false +} + type NotificationRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -380,7 +384,9 @@ type EventRequest struct { Queue string `protobuf:"bytes,1,opt,name=queue,proto3" json:"queue,omitempty"` // The node who is requesting the event stream. - Node string `protobuf:"bytes,2,opt,name=node,proto3" json:"node,omitempty"` + Node string `protobuf:"bytes,2,opt,name=node,proto3" json:"node,omitempty"` + WatcherName string `protobuf:"bytes,3,opt,name=watcher_name,json=watcherName,proto3" json:"watcher_name,omitempty"` + OrgId string `protobuf:"bytes,4,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` } func (x *EventRequest) Reset() { @@ -429,6 +435,20 @@ func (x *EventRequest) GetNode() string { return "" } +func (x *EventRequest) GetWatcherName() string { + if x != nil { + return x.WatcherName + } + return "" +} + +func (x *EventRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + type EventResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -496,6 +516,15 @@ type PushEventRequest struct { ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` FlowId string `protobuf:"bytes,3,opt,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` Jsonl []byte `protobuf:"bytes,4,opt,name=jsonl,proto3" json:"jsonl,omitempty"` + Rows int64 `protobuf:"varint,5,opt,name=rows,proto3" json:"rows,omitempty"` + OrgId string `protobuf:"bytes,6,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + // If false we do not write but just broadcast to all + // listeners. But if true we also write to the local filestore. + Write bool `protobuf:"varint,7,opt,name=write,proto3" json:"write,omitempty"` + // The username source for the event. This can only be set by the + // minion as a trusted impersonation. Other callers will have this + // set to their real username. + Username string `protobuf:"bytes,8,opt,name=username,proto3" json:"username,omitempty"` } func (x *PushEventRequest) Reset() { @@ -558,6 +587,34 @@ func (x *PushEventRequest) GetJsonl() []byte { return nil } +func (x *PushEventRequest) GetRows() int64 { + if x != nil { + return x.Rows + } + return 0 +} + +func (x *PushEventRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *PushEventRequest) GetWrite() bool { + if x != nil { + return x.Write + } + return false +} + +func (x *PushEventRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + var File_api_proto protoreflect.FileDescriptor var file_api_proto_rawDesc = []byte{ @@ -577,15 +634,19 @@ var file_api_proto_rawDesc = []byte{ 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x68, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0b, 0x68, 0x75, 0x6e, 0x74, 0x73, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0b, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x1a, 0x0f, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x73, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x1a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x09, 0x63, 0x73, 0x76, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0e, 0x64, 0x6f, 0x77, - 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x11, 0x63, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, - 0x76, 0x66, 0x73, 0x5f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, + 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0a, 0x64, 0x6f, 0x63, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0b, 0x68, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x0b, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, + 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x0b, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x09, 0x63, 0x73, + 0x76, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0e, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, + 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x11, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x76, 0x66, 0x73, 0x5f, + 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x73, 0x63, 0x68, 0x65, 0x64, + 0x75, 0x6c, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x73, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x12, 0x74, 0x69, 0x6d, 0x65, 0x6c, + 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, 0x11, 0x53, 0x74, 0x61, 0x72, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x22, 0x22, 0x0a, 0x08, 0x41, @@ -594,342 +655,437 @@ var file_api_proto_rawDesc = []byte{ 0x35, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x52, - 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xe4, 0x01, 0x0a, 0x1a, 0x56, 0x46, 0x53, 0x52, 0x65, + 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xc1, 0x01, 0x0a, 0x1a, 0x56, 0x46, 0x53, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x42, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x25, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1f, 0x0a, 0x06, 0x52, 0x44, 0x46, 0x55, 0x52, 0x4e, 0x12, 0x12, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x1a, 0x01, 0x02, 0x52, - 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x58, 0x58, 0x58, - 0x58, 0x76, 0x66, 0x73, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x58, 0x58, 0x58, 0x58, 0x76, 0x66, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x0e, - 0x76, 0x66, 0x73, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x76, 0x66, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, - 0x6e, 0x74, 0x73, 0x12, 0x38, 0x0a, 0x05, 0x64, 0x65, 0x70, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x04, 0x42, 0x22, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1c, 0x12, 0x1a, 0x44, 0x65, 0x70, 0x74, - 0x68, 0x20, 0x6f, 0x66, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x20, 0x72, - 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x05, 0x64, 0x65, 0x70, 0x74, 0x68, 0x22, 0x90, 0x01, - 0x0a, 0x0d, 0x56, 0x46, 0x53, 0x46, 0x69, 0x6c, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x12, - 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, - 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, - 0x66, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, - 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x06, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, - 0x22, 0x51, 0x0a, 0x13, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x5f, 0x61, - 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x79, - 0x41, 0x6c, 0x6c, 0x22, 0x38, 0x0a, 0x0c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x6f, 0x64, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x22, 0x3d, 0x0a, - 0x0d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, - 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6a, 0x73, 0x6f, 0x6e, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6a, 0x73, 0x6f, 0x6e, 0x6c, 0x22, 0x7a, 0x0a, 0x10, - 0x50, 0x75, 0x73, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x1b, 0x0a, 0x09, - 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, - 0x77, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, - 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6a, 0x73, 0x6f, 0x6e, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x05, 0x6a, 0x73, 0x6f, 0x6e, 0x6c, 0x32, 0xce, 0x2f, 0x0a, 0x03, 0x41, 0x50, 0x49, - 0x12, 0x52, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x75, 0x6e, 0x74, 0x12, 0x0b, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x22, 0x12, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x75, 0x6e, - 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x4e, 0x0a, 0x0c, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, - 0x48, 0x75, 0x6e, 0x74, 0x12, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, - 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x22, 0x14, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x48, 0x75, 0x6e, - 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x59, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, - 0x73, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, - 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x73, 0x12, - 0x46, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x22, 0x17, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x12, 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, - 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x12, 0x50, 0x0a, 0x0a, 0x4d, 0x6f, 0x64, 0x69, 0x66, - 0x79, 0x48, 0x75, 0x6e, 0x74, 0x12, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, - 0x6e, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x17, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4d, 0x6f, 0x64, 0x69, - 0x66, 0x79, 0x48, 0x75, 0x6e, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x5d, 0x0a, 0x0c, 0x47, 0x65, 0x74, - 0x48, 0x75, 0x6e, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x73, 0x12, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, - 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x16, 0x12, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x48, - 0x75, 0x6e, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x73, 0x12, 0x67, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x48, - 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x73, 0x12, 0x64, 0x0a, 0x0d, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x43, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x22, 0x14, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x5f, 0x0a, 0x0c, 0x4c, 0x61, 0x62, 0x65, 0x6c, - 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x50, 0x49, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x22, - 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x67, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, - 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x61, - 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x73, 0x12, 0x5d, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x17, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x66, 0x73, + 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0d, 0x76, 0x66, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, + 0x12, 0x38, 0x0a, 0x05, 0x64, 0x65, 0x70, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x42, + 0x22, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1c, 0x12, 0x1a, 0x44, 0x65, 0x70, 0x74, 0x68, 0x20, 0x6f, + 0x66, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x20, 0x72, 0x65, 0x66, 0x72, + 0x65, 0x73, 0x68, 0x52, 0x05, 0x64, 0x65, 0x70, 0x74, 0x68, 0x22, 0xd2, 0x01, 0x0a, 0x0d, 0x56, + 0x46, 0x53, 0x46, 0x69, 0x6c, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, + 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, + 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, + 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x15, 0x0a, + 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, + 0x72, 0x67, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, + 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x22, + 0x51, 0x0a, 0x13, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x5f, 0x61, 0x6c, + 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x41, + 0x6c, 0x6c, 0x22, 0x72, 0x0a, 0x0c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x0a, 0x0c, + 0x77, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x77, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0x3d, 0x0a, 0x0d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x6a, 0x73, 0x6f, 0x6e, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, + 0x6a, 0x73, 0x6f, 0x6e, 0x6c, 0x22, 0xd7, 0x01, 0x0a, 0x10, 0x50, 0x75, 0x73, 0x68, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x6a, 0x73, 0x6f, 0x6e, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6a, 0x73, 0x6f, + 0x6e, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x12, 0x14, 0x0a, + 0x05, 0x77, 0x72, 0x69, 0x74, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x77, 0x72, + 0x69, 0x74, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x32, + 0xad, 0x3f, 0x0a, 0x03, 0x41, 0x50, 0x49, 0x12, 0x52, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x48, 0x75, 0x6e, 0x74, 0x12, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, + 0x6e, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, + 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x17, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x48, 0x75, 0x6e, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x5d, 0x0a, 0x0c, 0x45, + 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x48, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x1f, 0x12, 0x1d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x7b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, - 0x12, 0x72, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, - 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x2d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x27, 0x12, 0x25, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x7b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x68, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x48, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x19, 0x22, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x45, 0x73, 0x74, 0x69, 0x6d, + 0x61, 0x74, 0x65, 0x48, 0x75, 0x6e, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x5d, 0x0a, 0x0c, 0x47, 0x65, + 0x74, 0x48, 0x75, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x16, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, + 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x16, 0x12, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, + 0x48, 0x75, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x59, 0x0a, 0x09, 0x4c, 0x69, 0x73, + 0x74, 0x48, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4c, 0x69, 0x73, 0x74, 0x48, + 0x75, 0x6e, 0x74, 0x73, 0x12, 0x46, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x12, + 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, + 0x75, 0x6e, 0x74, 0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x12, 0x0f, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x12, 0x53, 0x0a, 0x0b, + 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x54, 0x61, 0x67, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, + 0x54, 0x61, 0x67, 0x73, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x54, 0x61, 0x67, + 0x73, 0x12, 0x58, 0x0a, 0x0a, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x48, 0x75, 0x6e, 0x74, 0x12, + 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x4d, 0x75, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1d, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x17, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4d, 0x6f, + 0x64, 0x69, 0x66, 0x79, 0x48, 0x75, 0x6e, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x5d, 0x0a, 0x0c, 0x47, + 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x73, 0x12, 0x16, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x54, + 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x16, 0x12, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, + 0x74, 0x48, 0x75, 0x6e, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x73, 0x12, 0x67, 0x0a, 0x0e, 0x47, 0x65, + 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1c, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x73, 0x12, 0x64, 0x0a, 0x0d, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, - 0x22, 0x19, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x74, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0x01, 0x2a, 0x12, 0x99, - 0x01, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x46, 0x6c, 0x6f, 0x77, - 0x73, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, - 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x58, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x52, 0x12, 0x22, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, - 0x31, 0x2f, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x73, - 0x2f, 0x7b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x5a, 0x2c, 0x42, 0x2a, - 0x0a, 0x04, 0x48, 0x45, 0x41, 0x44, 0x12, 0x22, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, - 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x73, 0x2f, 0x7b, - 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x5d, 0x0a, 0x0f, 0x47, 0x65, - 0x74, 0x55, 0x73, 0x65, 0x72, 0x55, 0x49, 0x54, 0x72, 0x61, 0x69, 0x74, 0x73, 0x12, 0x16, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, - 0x69, 0x47, 0x72, 0x72, 0x55, 0x73, 0x65, 0x72, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, + 0x22, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x5f, 0x0a, 0x0c, 0x4c, 0x61, 0x62, + 0x65, 0x6c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x50, + 0x49, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x19, 0x22, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4c, 0x61, 0x62, 0x65, 0x6c, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x67, 0x0a, 0x0b, 0x4c, 0x69, + 0x73, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x73, 0x12, 0x5d, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x22, 0x25, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x7b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, + 0x64, 0x7d, 0x12, 0x72, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x2d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x27, 0x12, + 0x25, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x7b, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x72, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1f, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x22, 0x19, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0x01, 0x2a, 0x12, 0x61, 0x0a, 0x0e, 0x47, 0x65, + 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x73, 0x12, 0x16, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, + 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, + 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x73, 0x12, 0x5a, 0x0a, + 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x55, 0x49, 0x54, 0x72, 0x61, 0x69, 0x74, 0x73, + 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x41, 0x70, 0x69, 0x55, 0x73, 0x65, 0x72, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, - 0x72, 0x55, 0x49, 0x54, 0x72, 0x61, 0x69, 0x74, 0x73, 0x12, 0x66, 0x0a, 0x0d, 0x53, 0x65, 0x74, + 0x72, 0x55, 0x49, 0x54, 0x72, 0x61, 0x69, 0x74, 0x73, 0x12, 0x6c, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x47, 0x55, 0x49, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x74, 0x47, 0x55, 0x49, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, - 0x2f, 0x53, 0x65, 0x74, 0x47, 0x55, 0x49, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x01, - 0x2a, 0x12, 0x4a, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x16, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, - 0x65, 0x72, 0x73, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x57, 0x0a, - 0x10, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x46, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, - 0x73, 0x12, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x61, 0x76, 0x6f, 0x72, 0x69, - 0x74, 0x65, 0x1a, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x61, 0x76, 0x6f, 0x72, - 0x69, 0x74, 0x65, 0x73, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x12, 0x18, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x46, 0x61, 0x76, - 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x6f, 0x0a, 0x10, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, - 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x26, 0x12, 0x24, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x56, 0x46, 0x53, 0x4c, 0x69, - 0x73, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x2f, 0x7b, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x82, 0x01, 0x0a, 0x13, 0x56, 0x46, 0x53, 0x52, - 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, - 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x52, 0x65, 0x66, 0x72, 0x65, - 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x26, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x20, 0x22, 0x1b, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x56, 0x46, 0x53, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, - 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x3a, 0x01, 0x2a, 0x12, 0x63, 0x0a, 0x10, - 0x56, 0x46, 0x53, 0x53, 0x74, 0x61, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x53, 0x65, 0x74, 0x47, 0x55, 0x49, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x15, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x74, 0x47, 0x55, 0x49, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x4a, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x55, 0x73, + 0x65, 0x72, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0c, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x73, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x12, 0x12, 0x10, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x55, 0x73, + 0x65, 0x72, 0x73, 0x12, 0x56, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, + 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0c, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x73, 0x22, 0x1e, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, + 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x52, 0x0a, 0x0c, 0x47, + 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x12, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, + 0x73, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x12, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, + 0x59, 0x0a, 0x0c, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, + 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, + 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x19, 0x22, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x74, 0x55, 0x73, + 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x56, 0x0a, 0x07, 0x47, 0x65, + 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x56, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x55, 0x73, + 0x65, 0x72, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, + 0x65, 0x7d, 0x12, 0x5d, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, + 0x12, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x3a, 0x01, + 0x2a, 0x12, 0x57, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x46, 0x61, 0x76, 0x6f, + 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x61, + 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x1a, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, + 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, + 0x12, 0x18, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x46, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x60, 0x0a, 0x0b, 0x53, 0x65, + 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1e, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x18, 0x22, 0x13, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, + 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x3a, 0x01, 0x2a, 0x12, 0x6f, 0x0a, 0x10, + 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x12, 0x18, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, - 0x2f, 0x56, 0x46, 0x53, 0x53, 0x74, 0x61, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x79, 0x12, 0x69, 0x0a, 0x0f, 0x56, 0x46, 0x53, 0x53, 0x74, 0x61, 0x74, 0x44, 0x6f, 0x77, 0x6e, - 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, - 0x53, 0x74, 0x61, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x44, - 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x1f, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x56, 0x46, 0x53, - 0x53, 0x74, 0x61, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x55, 0x0a, 0x08, - 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x12, 0x12, 0x10, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x54, 0x61, - 0x62, 0x6c, 0x65, 0x12, 0x75, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x41, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, - 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x41, 0x72, 0x67, 0x73, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, - 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x22, 0x17, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x41, - 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x5c, 0x0a, 0x0a, 0x43, 0x61, - 0x6e, 0x63, 0x65, 0x6c, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x46, 0x6c, 0x6f, - 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x17, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x43, 0x61, 0x6e, 0x63, 0x65, - 0x6c, 0x46, 0x6c, 0x6f, 0x77, 0x3a, 0x01, 0x2a, 0x12, 0x5e, 0x0a, 0x0b, 0x41, 0x72, 0x63, 0x68, - 0x69, 0x76, 0x65, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x46, 0x6c, 0x6f, 0x77, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, - 0x22, 0x13, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, - 0x65, 0x46, 0x6c, 0x6f, 0x77, 0x3a, 0x01, 0x2a, 0x12, 0x5b, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x46, - 0x6c, 0x6f, 0x77, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x6c, 0x6f, 0x77, 0x44, 0x65, - 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x44, 0x65, - 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x67, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x46, 0x6c, 0x6f, 0x77, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x1f, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, - 0x65, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x71, - 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, - 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x4b, - 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x67, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, - 0x73, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, - 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x44, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x73, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x19, 0x22, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x41, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x69, 0x0a, 0x0f, 0x47, 0x65, - 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x19, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, - 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x64, 0x0a, 0x0f, 0x53, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, - 0x66, 0x61, 0x63, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x53, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x50, 0x49, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x22, - 0x17, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, - 0x66, 0x61, 0x63, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x6e, 0x0a, 0x10, 0x4c, - 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x12, - 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x46, 0x69, 0x6c, 0x65, 0x42, - 0x75, 0x66, 0x66, 0x65, 0x72, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, - 0x61, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x23, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x22, 0x18, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x69, - 0x66, 0x61, 0x63, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x3a, 0x01, 0x2a, 0x12, 0x44, 0x0a, 0x0b, 0x47, + 0x2c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x26, 0x12, 0x24, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x79, 0x2f, 0x7b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x6f, 0x0a, + 0x15, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x79, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, + 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, + 0x1d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, + 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x82, + 0x01, 0x0a, 0x13, 0x56, 0x46, 0x53, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, + 0x46, 0x53, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x26, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x20, 0x22, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x56, 0x46, 0x53, + 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, + 0x3a, 0x01, 0x2a, 0x12, 0x63, 0x0a, 0x10, 0x56, 0x46, 0x53, 0x53, 0x74, 0x61, 0x74, 0x44, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x12, 0x18, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x56, 0x46, 0x53, 0x53, 0x74, 0x61, 0x74, 0x44, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x69, 0x0a, 0x0f, 0x56, 0x46, 0x53, 0x53, + 0x74, 0x61, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1d, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x53, 0x74, 0x61, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, + 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x6e, + 0x66, 0x6f, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x56, 0x46, 0x53, 0x53, 0x74, 0x61, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, + 0x6f, 0x61, 0x64, 0x12, 0x6e, 0x0a, 0x0f, 0x56, 0x46, 0x53, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, + 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, + 0x46, 0x53, 0x53, 0x74, 0x61, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x74, + 0x61, 0x72, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x22, 0x17, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x56, 0x46, 0x53, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, + 0x3a, 0x01, 0x2a, 0x12, 0x55, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, + 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, + 0x31, 0x2f, 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x60, 0x0a, 0x0a, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x46, 0x69, 0x6c, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x75, 0x0a, 0x0f, + 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, + 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x20, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x22, 0x17, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x3a, 0x01, 0x2a, 0x12, 0x5c, 0x0a, 0x0a, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x46, 0x6c, 0x6f, + 0x77, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, + 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x46, 0x6c, 0x6f, 0x77, 0x3a, 0x01, + 0x2a, 0x12, 0x5a, 0x0a, 0x0a, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x46, 0x6c, 0x6f, 0x77, 0x12, + 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1d, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, + 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x46, 0x6c, 0x6f, 0x77, 0x3a, 0x01, 0x2a, 0x12, 0x5b, 0x0a, + 0x0e, 0x47, 0x65, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, + 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, + 0x6c, 0x6f, 0x77, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x18, 0x12, 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x46, + 0x6c, 0x6f, 0x77, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x67, 0x0a, 0x0f, 0x47, 0x65, + 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x15, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, + 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, + 0x6c, 0x73, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x73, 0x12, 0x71, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, + 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, + 0x77, 0x6f, 0x72, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, + 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x63, 0x0a, 0x0b, 0x52, 0x65, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x56, 0x51, 0x4c, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x56, 0x51, 0x4c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x74, 0x56, 0x51, 0x4c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x18, 0x22, 0x13, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x52, 0x65, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x56, 0x51, 0x4c, 0x3a, 0x01, 0x2a, 0x12, 0x67, 0x0a, 0x0c, 0x47, + 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x6f, 0x72, 0x73, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x22, 0x14, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x69, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, + 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, + 0x6c, 0x0a, 0x0f, 0x53, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x46, 0x69, + 0x6c, 0x65, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x74, 0x41, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x1c, 0x22, 0x17, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x74, 0x41, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x78, 0x0a, + 0x10, 0x4c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x50, 0x61, 0x63, + 0x6b, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x41, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x41, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x23, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x22, 0x18, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x4c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x50, 0x61, 0x63, 0x6b, 0x3a, 0x01, 0x2a, 0x12, 0x5c, 0x0a, 0x0a, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x44, 0x6f, 0x63, 0x73, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x6f, + 0x63, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x6f, 0x63, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x14, 0x12, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x44, 0x6f, 0x63, 0x73, 0x12, 0x44, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x6f, 0x6c, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x6f, 0x6f, + 0x6c, 0x1a, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x6f, 0x6f, 0x6c, 0x22, 0x1b, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, + 0x47, 0x65, 0x74, 0x54, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x47, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x54, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x6f, 0x6f, 0x6c, 0x1a, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x54, 0x6f, 0x6f, 0x6c, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x66, - 0x6f, 0x12, 0x47, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x54, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x66, 0x6f, - 0x12, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x6f, 0x6f, 0x6c, 0x1a, 0x0b, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x6f, 0x6f, 0x6c, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x18, 0x22, 0x13, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x74, 0x54, - 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x3a, 0x01, 0x2a, 0x12, 0x5c, 0x0a, 0x09, 0x47, 0x65, - 0x74, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x16, 0x22, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x7a, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, 0x72, 0x67, 0x73, 0x22, 0x28, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x22, 0x12, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x83, 0x01, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, + 0x54, 0x6f, 0x6f, 0x6c, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x22, 0x13, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x74, 0x54, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x66, + 0x6f, 0x3a, 0x01, 0x2a, 0x12, 0x5c, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x22, 0x11, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x3a, + 0x01, 0x2a, 0x12, 0x7a, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, + 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x16, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, + 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x41, 0x72, 0x67, 0x73, 0x22, 0x28, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x22, 0x12, 0x20, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, + 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x83, + 0x01, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, 0x6f, 0x6e, 0x69, + 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x41, 0x72, 0x67, 0x73, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x22, + 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, 0x72, 0x67, 0x73, 0x1a, - 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, - 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, 0x72, 0x67, 0x73, 0x22, 0x2b, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x22, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, - 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, - 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x85, 0x01, 0x0a, 0x18, 0x47, - 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, - 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, - 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x28, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x22, - 0x12, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, + 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x85, 0x01, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x78, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, - 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x22, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, - 0x2f, 0x53, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, - 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x9c, 0x01, 0x0a, - 0x19, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x26, 0x22, 0x21, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4c, - 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x74, 0x0a, 0x12, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, - 0x65, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, - 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x21, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x22, 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x3a, 0x01, - 0x2a, 0x12, 0x5a, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, - 0x73, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, - 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x73, 0x22, - 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x12, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, - 0x2f, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x5f, 0x0a, - 0x0b, 0x4e, 0x65, 0x77, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x12, 0x17, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, - 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x1e, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x22, 0x13, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, - 0x4e, 0x65, 0x77, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x3a, 0x01, 0x2a, 0x12, 0x65, - 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, - 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, - 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x22, 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x22, 0x16, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x76, 0x31, 0x2f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, + 0x6c, 0x65, 0x22, 0x28, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x22, 0x12, 0x20, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x6f, 0x6e, + 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x78, 0x0a, 0x18, + 0x53, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, + 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, + 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x25, 0x22, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x53, 0x65, 0x74, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x9c, 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, + 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x26, 0x22, + 0x21, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, + 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x74, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, + 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x1c, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, + 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, + 0x22, 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x3a, 0x01, 0x2a, 0x12, 0x5a, 0x0a, 0x0c, 0x47, + 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x1a, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x73, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x16, 0x12, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x4e, 0x6f, + 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x5f, 0x0a, 0x0b, 0x4e, 0x65, 0x77, 0x4e, 0x6f, + 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, + 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, + 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, + 0x22, 0x13, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x4e, 0x65, 0x77, 0x4e, 0x6f, 0x74, + 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x3a, 0x01, 0x2a, 0x12, 0x65, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, + 0x62, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x21, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x1b, 0x22, 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x3a, 0x01, 0x2a, 0x12, + 0x64, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, + 0x6b, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, + 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x22, 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x22, 0x16, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x3a, 0x01, 0x2a, 0x12, 0x6a, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, @@ -950,80 +1106,122 @@ var file_api_proto_rawDesc = []byte{ 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x22, 0x1a, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, - 0x6c, 0x3a, 0x01, 0x2a, 0x12, 0x6f, 0x0a, 0x12, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4e, 0x6f, + 0x6c, 0x3a, 0x01, 0x2a, 0x12, 0x6c, 0x0a, 0x12, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x25, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x22, 0x1a, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, - 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, - 0x6c, 0x6c, 0x3a, 0x01, 0x2a, 0x12, 0x81, 0x01, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, + 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x22, 0x25, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x1f, 0x22, 0x1a, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x52, 0x65, 0x76, + 0x65, 0x72, 0x74, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x3a, + 0x01, 0x2a, 0x12, 0x6f, 0x0a, 0x12, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4e, 0x6f, 0x74, 0x65, + 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x25, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x1f, 0x22, 0x1a, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x43, 0x61, + 0x6e, 0x63, 0x65, 0x6c, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, + 0x3a, 0x01, 0x2a, 0x12, 0x81, 0x01, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x6f, + 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, + 0x6c, 0x65, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, + 0x6f, 0x6f, 0x6b, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x2d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x27, + 0x22, 0x22, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, - 0x46, 0x69, 0x6c, 0x65, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, - 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x2d, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x27, 0x22, 0x22, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, - 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x8c, 0x01, 0x0a, 0x18, 0x55, 0x70, - 0x6c, 0x6f, 0x61, 0x64, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x41, 0x74, 0x74, 0x61, - 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, + 0x46, 0x69, 0x6c, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x8c, 0x01, 0x0a, 0x18, 0x55, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, + 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, + 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x55, 0x70, 0x6c, 0x6f, 0x61, - 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x55, 0x70, 0x6c, - 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x25, 0x22, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x55, 0x70, 0x6c, - 0x6f, 0x61, 0x64, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, - 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x69, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, - 0x72, 0x74, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x45, 0x78, 0x70, 0x6f, 0x72, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x25, 0x22, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x55, 0x70, 0x6c, 0x6f, 0x61, + 0x64, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, + 0x65, 0x6e, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x81, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, + 0x62, 0x6f, 0x6f, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x2b, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x22, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x52, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x41, 0x74, 0x74, + 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x69, 0x0a, 0x10, 0x41, 0x6e, + 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x18, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x22, 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, - 0x31, 0x2f, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, - 0x3a, 0x01, 0x2a, 0x12, 0x3c, 0x0a, 0x0c, 0x56, 0x46, 0x53, 0x47, 0x65, 0x74, 0x42, 0x75, 0x66, - 0x66, 0x65, 0x72, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x46, - 0x69, 0x6c, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x1a, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x46, 0x69, 0x6c, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x22, - 0x00, 0x12, 0x38, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, - 0x72, 0x67, 0x73, 0x1a, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3b, 0x0a, 0x0a, 0x57, - 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3f, 0x0a, 0x0a, 0x50, 0x75, 0x73, 0x68, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, - 0x75, 0x73, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x0a, 0x57, 0x72, 0x69, - 0x74, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x56, 0x51, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x61, 0x74, 0x61, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x37, - 0x0a, 0x0a, 0x53, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x2e, 0x70, + 0x22, 0x23, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x22, 0x18, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, + 0x31, 0x2f, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6c, 0x69, + 0x6e, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x71, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, + 0x73, 0x74, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x44, 0x65, 0x66, + 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x50, 0x0a, 0x09, 0x41, 0x64, 0x64, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x0d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1c, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x16, 0x22, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x41, 0x64, + 0x64, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x63, 0x0a, 0x0c, 0x4d, 0x6f, + 0x64, 0x69, 0x66, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1f, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x22, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, + 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x3a, 0x01, 0x2a, 0x12, + 0x44, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x0d, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x1a, 0x0d, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x47, 0x65, 0x74, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x3c, 0x0a, 0x0c, 0x56, 0x46, 0x53, 0x47, 0x65, 0x74, 0x42, + 0x75, 0x66, 0x66, 0x65, 0x72, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, + 0x53, 0x46, 0x69, 0x6c, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x1a, 0x14, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x46, 0x53, 0x46, 0x69, 0x6c, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, + 0x72, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x17, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x51, + 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3b, 0x0a, + 0x0a, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x13, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3f, 0x0a, 0x0a, 0x50, 0x75, + 0x73, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x50, 0x75, 0x73, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x0a, 0x57, + 0x72, 0x69, 0x74, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x53, 0x63, 0x68, 0x65, 0x64, + 0x75, 0x6c, 0x65, 0x72, 0x12, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x63, 0x68, + 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x37, 0x0a, 0x0a, 0x47, + 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x0a, 0x53, 0x65, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, + 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, + 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0c, + 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, - 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, - 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x05, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, - 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, - 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, - 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x69, + 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x3e, 0x0a, 0x05, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, + 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1050,195 +1248,259 @@ var file_api_proto_goTypes = []interface{}{ (*EventResponse)(nil), // 7: proto.EventResponse (*PushEventRequest)(nil), // 8: proto.PushEventRequest (*Hunt)(nil), // 9: proto.Hunt - (*ListHuntsRequest)(nil), // 10: proto.ListHuntsRequest - (*GetHuntRequest)(nil), // 11: proto.GetHuntRequest - (*GetTableRequest)(nil), // 12: proto.GetTableRequest - (*GetHuntResultsRequest)(nil), // 13: proto.GetHuntResultsRequest - (*LabelClientsRequest)(nil), // 14: proto.LabelClientsRequest - (*SearchClientsRequest)(nil), // 15: proto.SearchClientsRequest - (*GetClientRequest)(nil), // 16: proto.GetClientRequest - (*ClientMetadata)(nil), // 17: proto.ClientMetadata - (*ApiFlowRequest)(nil), // 18: proto.ApiFlowRequest - (*empty.Empty)(nil), // 19: google.protobuf.Empty - (*SetGUIOptionsRequest)(nil), // 20: proto.SetGUIOptionsRequest - (*Favorite)(nil), // 21: proto.Favorite - (*VFSListRequest)(nil), // 22: proto.VFSListRequest - (*VFSStatDownloadRequest)(nil), // 23: proto.VFSStatDownloadRequest - (*proto.ArtifactCollectorArgs)(nil), // 24: proto.ArtifactCollectorArgs - (*GetArtifactsRequest)(nil), // 25: proto.GetArtifactsRequest - (*GetArtifactRequest)(nil), // 26: proto.GetArtifactRequest - (*SetArtifactRequest)(nil), // 27: proto.SetArtifactRequest - (*proto1.Tool)(nil), // 28: proto.Tool - (*GetReportRequest)(nil), // 29: proto.GetReportRequest - (*proto.GetClientMonitoringStateRequest)(nil), // 30: proto.GetClientMonitoringStateRequest - (*proto.ClientEventTable)(nil), // 31: proto.ClientEventTable - (*ListAvailableEventResultsRequest)(nil), // 32: proto.ListAvailableEventResultsRequest - (*CreateDownloadRequest)(nil), // 33: proto.CreateDownloadRequest - (*NotebookCellRequest)(nil), // 34: proto.NotebookCellRequest - (*NotebookMetadata)(nil), // 35: proto.NotebookMetadata - (*NotebookExportRequest)(nil), // 36: proto.NotebookExportRequest - (*NotebookFileUploadRequest)(nil), // 37: proto.NotebookFileUploadRequest - (*proto2.VQLCollectorArgs)(nil), // 38: proto.VQLCollectorArgs - (*proto2.VQLResponse)(nil), // 39: proto.VQLResponse - (*DataRequest)(nil), // 40: proto.DataRequest - (*HealthCheckRequest)(nil), // 41: proto.HealthCheckRequest - (*HuntStats)(nil), // 42: proto.HuntStats - (*ListHuntsResponse)(nil), // 43: proto.ListHuntsResponse - (*GetTableResponse)(nil), // 44: proto.GetTableResponse - (*APIResponse)(nil), // 45: proto.APIResponse - (*SearchClientsResponse)(nil), // 46: proto.SearchClientsResponse - (*ApiClient)(nil), // 47: proto.ApiClient - (*ApiFlowResponse)(nil), // 48: proto.ApiFlowResponse - (*ApiGrrUser)(nil), // 49: proto.ApiGrrUser - (*Users)(nil), // 50: proto.Users - (*Favorites)(nil), // 51: proto.Favorites - (*VFSListResponse)(nil), // 52: proto.VFSListResponse - (*proto.ArtifactCollectorResponse)(nil), // 53: proto.ArtifactCollectorResponse - (*proto.VFSDownloadInfo)(nil), // 54: proto.VFSDownloadInfo - (*FlowDetails)(nil), // 55: proto.FlowDetails - (*ApiFlowRequestDetails)(nil), // 56: proto.ApiFlowRequestDetails - (*KeywordCompletions)(nil), // 57: proto.KeywordCompletions - (*proto1.ArtifactDescriptors)(nil), // 58: proto.ArtifactDescriptors - (*GetArtifactResponse)(nil), // 59: proto.GetArtifactResponse - (*LoadArtifactPackResponse)(nil), // 60: proto.LoadArtifactPackResponse - (*GetReportResponse)(nil), // 61: proto.GetReportResponse - (*ListAvailableEventResultsResponse)(nil), // 62: proto.ListAvailableEventResultsResponse - (*CreateDownloadResponse)(nil), // 63: proto.CreateDownloadResponse - (*Notebooks)(nil), // 64: proto.Notebooks - (*NotebookCell)(nil), // 65: proto.NotebookCell - (*NotebookFileUploadResponse)(nil), // 66: proto.NotebookFileUploadResponse - (*DataResponse)(nil), // 67: proto.DataResponse - (*ListChildrenResponse)(nil), // 68: proto.ListChildrenResponse - (*HealthCheckResponse)(nil), // 69: proto.HealthCheckResponse + (*HuntEstimateRequest)(nil), // 10: proto.HuntEstimateRequest + (*GetTableRequest)(nil), // 11: proto.GetTableRequest + (*ListHuntsRequest)(nil), // 12: proto.ListHuntsRequest + (*GetHuntRequest)(nil), // 13: proto.GetHuntRequest + (*emptypb.Empty)(nil), // 14: google.protobuf.Empty + (*HuntMutation)(nil), // 15: proto.HuntMutation + (*GetHuntResultsRequest)(nil), // 16: proto.GetHuntResultsRequest + (*LabelClientsRequest)(nil), // 17: proto.LabelClientsRequest + (*SearchClientsRequest)(nil), // 18: proto.SearchClientsRequest + (*GetClientRequest)(nil), // 19: proto.GetClientRequest + (*SetClientMetadataRequest)(nil), // 20: proto.SetClientMetadataRequest + (*SetGUIOptionsRequest)(nil), // 21: proto.SetGUIOptionsRequest + (*UserRequest)(nil), // 22: proto.UserRequest + (*UserRoles)(nil), // 23: proto.UserRoles + (*UpdateUserRequest)(nil), // 24: proto.UpdateUserRequest + (*Favorite)(nil), // 25: proto.Favorite + (*SetPasswordRequest)(nil), // 26: proto.SetPasswordRequest + (*VFSListRequest)(nil), // 27: proto.VFSListRequest + (*VFSStatDownloadRequest)(nil), // 28: proto.VFSStatDownloadRequest + (*SearchFileRequest)(nil), // 29: proto.SearchFileRequest + (*proto.ArtifactCollectorArgs)(nil), // 30: proto.ArtifactCollectorArgs + (*ApiFlowRequest)(nil), // 31: proto.ApiFlowRequest + (*ReformatVQLMessage)(nil), // 32: proto.ReformatVQLMessage + (*GetArtifactsRequest)(nil), // 33: proto.GetArtifactsRequest + (*GetArtifactRequest)(nil), // 34: proto.GetArtifactRequest + (*SetArtifactRequest)(nil), // 35: proto.SetArtifactRequest + (*LoadArtifactPackRequest)(nil), // 36: proto.LoadArtifactPackRequest + (*DocSearchRequest)(nil), // 37: proto.DocSearchRequest + (*proto1.Tool)(nil), // 38: proto.Tool + (*GetReportRequest)(nil), // 39: proto.GetReportRequest + (*proto.GetClientMonitoringStateRequest)(nil), // 40: proto.GetClientMonitoringStateRequest + (*proto.ClientEventTable)(nil), // 41: proto.ClientEventTable + (*ListAvailableEventResultsRequest)(nil), // 42: proto.ListAvailableEventResultsRequest + (*CreateDownloadRequest)(nil), // 43: proto.CreateDownloadRequest + (*NotebookCellRequest)(nil), // 44: proto.NotebookCellRequest + (*NotebookMetadata)(nil), // 45: proto.NotebookMetadata + (*NotebookExportRequest)(nil), // 46: proto.NotebookExportRequest + (*NotebookFileUploadRequest)(nil), // 47: proto.NotebookFileUploadRequest + (*AnnotationRequest)(nil), // 48: proto.AnnotationRequest + (*Secret)(nil), // 49: proto.Secret + (*ModifySecretRequest)(nil), // 50: proto.ModifySecretRequest + (*proto2.VQLCollectorArgs)(nil), // 51: proto.VQLCollectorArgs + (*proto2.VQLResponse)(nil), // 52: proto.VQLResponse + (*ScheduleRequest)(nil), // 53: proto.ScheduleRequest + (*DataRequest)(nil), // 54: proto.DataRequest + (*HealthCheckRequest)(nil), // 55: proto.HealthCheckRequest + (*HuntStats)(nil), // 56: proto.HuntStats + (*GetTableResponse)(nil), // 57: proto.GetTableResponse + (*ListHuntsResponse)(nil), // 58: proto.ListHuntsResponse + (*HuntTags)(nil), // 59: proto.HuntTags + (*APIResponse)(nil), // 60: proto.APIResponse + (*SearchClientsResponse)(nil), // 61: proto.SearchClientsResponse + (*ApiClient)(nil), // 62: proto.ApiClient + (*ClientMetadata)(nil), // 63: proto.ClientMetadata + (*ApiUser)(nil), // 64: proto.ApiUser + (*SetGUIOptionsResponse)(nil), // 65: proto.SetGUIOptionsResponse + (*Users)(nil), // 66: proto.Users + (*VelociraptorUser)(nil), // 67: proto.VelociraptorUser + (*Favorites)(nil), // 68: proto.Favorites + (*VFSListResponse)(nil), // 69: proto.VFSListResponse + (*proto.ArtifactCollectorResponse)(nil), // 70: proto.ArtifactCollectorResponse + (*proto.VFSDownloadInfo)(nil), // 71: proto.VFSDownloadInfo + (*SearchFileResponse)(nil), // 72: proto.SearchFileResponse + (*FlowDetails)(nil), // 73: proto.FlowDetails + (*ApiFlowRequestDetails)(nil), // 74: proto.ApiFlowRequestDetails + (*KeywordCompletions)(nil), // 75: proto.KeywordCompletions + (*proto1.ArtifactDescriptors)(nil), // 76: proto.ArtifactDescriptors + (*GetArtifactResponse)(nil), // 77: proto.GetArtifactResponse + (*SetArtifactResponse)(nil), // 78: proto.SetArtifactResponse + (*LoadArtifactPackResponse)(nil), // 79: proto.LoadArtifactPackResponse + (*DocSearchResponses)(nil), // 80: proto.DocSearchResponses + (*GetReportResponse)(nil), // 81: proto.GetReportResponse + (*ListAvailableEventResultsResponse)(nil), // 82: proto.ListAvailableEventResultsResponse + (*CreateDownloadResponse)(nil), // 83: proto.CreateDownloadResponse + (*Notebooks)(nil), // 84: proto.Notebooks + (*NotebookCell)(nil), // 85: proto.NotebookCell + (*NotebookFileUploadResponse)(nil), // 86: proto.NotebookFileUploadResponse + (*SecretDefinitionList)(nil), // 87: proto.SecretDefinitionList + (*ScheduleResponse)(nil), // 88: proto.ScheduleResponse + (*DataResponse)(nil), // 89: proto.DataResponse + (*ListChildrenResponse)(nil), // 90: proto.ListChildrenResponse + (*HealthCheckResponse)(nil), // 91: proto.HealthCheckResponse } var file_api_proto_depIdxs = []int32{ 1, // 0: proto.ApprovalList.items:type_name -> proto.Approval 9, // 1: proto.API.CreateHunt:input_type -> proto.Hunt - 9, // 2: proto.API.EstimateHunt:input_type -> proto.Hunt - 10, // 3: proto.API.ListHunts:input_type -> proto.ListHuntsRequest - 11, // 4: proto.API.GetHunt:input_type -> proto.GetHuntRequest - 9, // 5: proto.API.ModifyHunt:input_type -> proto.Hunt - 12, // 6: proto.API.GetHuntFlows:input_type -> proto.GetTableRequest - 13, // 7: proto.API.GetHuntResults:input_type -> proto.GetHuntResultsRequest - 5, // 8: proto.API.NotifyClients:input_type -> proto.NotificationRequest - 14, // 9: proto.API.LabelClients:input_type -> proto.LabelClientsRequest - 15, // 10: proto.API.ListClients:input_type -> proto.SearchClientsRequest - 16, // 11: proto.API.GetClient:input_type -> proto.GetClientRequest - 16, // 12: proto.API.GetClientMetadata:input_type -> proto.GetClientRequest - 17, // 13: proto.API.SetClientMetadata:input_type -> proto.ClientMetadata - 18, // 14: proto.API.GetClientFlows:input_type -> proto.ApiFlowRequest - 19, // 15: proto.API.GetUserUITraits:input_type -> google.protobuf.Empty - 20, // 16: proto.API.SetGUIOptions:input_type -> proto.SetGUIOptionsRequest - 19, // 17: proto.API.GetUsers:input_type -> google.protobuf.Empty - 21, // 18: proto.API.GetUserFavorites:input_type -> proto.Favorite - 22, // 19: proto.API.VFSListDirectory:input_type -> proto.VFSListRequest - 3, // 20: proto.API.VFSRefreshDirectory:input_type -> proto.VFSRefreshDirectoryRequest - 22, // 21: proto.API.VFSStatDirectory:input_type -> proto.VFSListRequest - 23, // 22: proto.API.VFSStatDownload:input_type -> proto.VFSStatDownloadRequest - 12, // 23: proto.API.GetTable:input_type -> proto.GetTableRequest - 24, // 24: proto.API.CollectArtifact:input_type -> proto.ArtifactCollectorArgs - 18, // 25: proto.API.CancelFlow:input_type -> proto.ApiFlowRequest - 18, // 26: proto.API.ArchiveFlow:input_type -> proto.ApiFlowRequest - 18, // 27: proto.API.GetFlowDetails:input_type -> proto.ApiFlowRequest - 18, // 28: proto.API.GetFlowRequests:input_type -> proto.ApiFlowRequest - 19, // 29: proto.API.GetKeywordCompletions:input_type -> google.protobuf.Empty - 25, // 30: proto.API.GetArtifacts:input_type -> proto.GetArtifactsRequest - 26, // 31: proto.API.GetArtifactFile:input_type -> proto.GetArtifactRequest - 27, // 32: proto.API.SetArtifactFile:input_type -> proto.SetArtifactRequest - 4, // 33: proto.API.LoadArtifactPack:input_type -> proto.VFSFileBuffer - 28, // 34: proto.API.GetToolInfo:input_type -> proto.Tool - 28, // 35: proto.API.SetToolInfo:input_type -> proto.Tool - 29, // 36: proto.API.GetReport:input_type -> proto.GetReportRequest - 19, // 37: proto.API.GetServerMonitoringState:input_type -> google.protobuf.Empty - 24, // 38: proto.API.SetServerMonitoringState:input_type -> proto.ArtifactCollectorArgs - 30, // 39: proto.API.GetClientMonitoringState:input_type -> proto.GetClientMonitoringStateRequest - 31, // 40: proto.API.SetClientMonitoringState:input_type -> proto.ClientEventTable - 32, // 41: proto.API.ListAvailableEventResults:input_type -> proto.ListAvailableEventResultsRequest - 33, // 42: proto.API.CreateDownloadFile:input_type -> proto.CreateDownloadRequest - 34, // 43: proto.API.GetNotebooks:input_type -> proto.NotebookCellRequest - 35, // 44: proto.API.NewNotebook:input_type -> proto.NotebookMetadata - 35, // 45: proto.API.UpdateNotebook:input_type -> proto.NotebookMetadata - 34, // 46: proto.API.NewNotebookCell:input_type -> proto.NotebookCellRequest - 34, // 47: proto.API.GetNotebookCell:input_type -> proto.NotebookCellRequest - 34, // 48: proto.API.UpdateNotebookCell:input_type -> proto.NotebookCellRequest - 34, // 49: proto.API.CancelNotebookCell:input_type -> proto.NotebookCellRequest - 36, // 50: proto.API.CreateNotebookDownloadFile:input_type -> proto.NotebookExportRequest - 37, // 51: proto.API.UploadNotebookAttachment:input_type -> proto.NotebookFileUploadRequest - 36, // 52: proto.API.ExportNotebook:input_type -> proto.NotebookExportRequest - 4, // 53: proto.API.VFSGetBuffer:input_type -> proto.VFSFileBuffer - 38, // 54: proto.API.Query:input_type -> proto.VQLCollectorArgs - 6, // 55: proto.API.WatchEvent:input_type -> proto.EventRequest - 8, // 56: proto.API.PushEvents:input_type -> proto.PushEventRequest - 39, // 57: proto.API.WriteEvent:input_type -> proto.VQLResponse - 40, // 58: proto.API.GetSubject:input_type -> proto.DataRequest - 40, // 59: proto.API.SetSubject:input_type -> proto.DataRequest - 40, // 60: proto.API.DeleteSubject:input_type -> proto.DataRequest - 40, // 61: proto.API.ListChildren:input_type -> proto.DataRequest - 41, // 62: proto.API.Check:input_type -> proto.HealthCheckRequest - 0, // 63: proto.API.CreateHunt:output_type -> proto.StartFlowResponse - 42, // 64: proto.API.EstimateHunt:output_type -> proto.HuntStats - 43, // 65: proto.API.ListHunts:output_type -> proto.ListHuntsResponse - 9, // 66: proto.API.GetHunt:output_type -> proto.Hunt - 19, // 67: proto.API.ModifyHunt:output_type -> google.protobuf.Empty - 44, // 68: proto.API.GetHuntFlows:output_type -> proto.GetTableResponse - 44, // 69: proto.API.GetHuntResults:output_type -> proto.GetTableResponse - 19, // 70: proto.API.NotifyClients:output_type -> google.protobuf.Empty - 45, // 71: proto.API.LabelClients:output_type -> proto.APIResponse - 46, // 72: proto.API.ListClients:output_type -> proto.SearchClientsResponse - 47, // 73: proto.API.GetClient:output_type -> proto.ApiClient - 17, // 74: proto.API.GetClientMetadata:output_type -> proto.ClientMetadata - 19, // 75: proto.API.SetClientMetadata:output_type -> google.protobuf.Empty - 48, // 76: proto.API.GetClientFlows:output_type -> proto.ApiFlowResponse - 49, // 77: proto.API.GetUserUITraits:output_type -> proto.ApiGrrUser - 19, // 78: proto.API.SetGUIOptions:output_type -> google.protobuf.Empty - 50, // 79: proto.API.GetUsers:output_type -> proto.Users - 51, // 80: proto.API.GetUserFavorites:output_type -> proto.Favorites - 52, // 81: proto.API.VFSListDirectory:output_type -> proto.VFSListResponse - 53, // 82: proto.API.VFSRefreshDirectory:output_type -> proto.ArtifactCollectorResponse - 52, // 83: proto.API.VFSStatDirectory:output_type -> proto.VFSListResponse - 54, // 84: proto.API.VFSStatDownload:output_type -> proto.VFSDownloadInfo - 44, // 85: proto.API.GetTable:output_type -> proto.GetTableResponse - 53, // 86: proto.API.CollectArtifact:output_type -> proto.ArtifactCollectorResponse - 0, // 87: proto.API.CancelFlow:output_type -> proto.StartFlowResponse - 0, // 88: proto.API.ArchiveFlow:output_type -> proto.StartFlowResponse - 55, // 89: proto.API.GetFlowDetails:output_type -> proto.FlowDetails - 56, // 90: proto.API.GetFlowRequests:output_type -> proto.ApiFlowRequestDetails - 57, // 91: proto.API.GetKeywordCompletions:output_type -> proto.KeywordCompletions - 58, // 92: proto.API.GetArtifacts:output_type -> proto.ArtifactDescriptors - 59, // 93: proto.API.GetArtifactFile:output_type -> proto.GetArtifactResponse - 45, // 94: proto.API.SetArtifactFile:output_type -> proto.APIResponse - 60, // 95: proto.API.LoadArtifactPack:output_type -> proto.LoadArtifactPackResponse - 28, // 96: proto.API.GetToolInfo:output_type -> proto.Tool - 28, // 97: proto.API.SetToolInfo:output_type -> proto.Tool - 61, // 98: proto.API.GetReport:output_type -> proto.GetReportResponse - 24, // 99: proto.API.GetServerMonitoringState:output_type -> proto.ArtifactCollectorArgs - 24, // 100: proto.API.SetServerMonitoringState:output_type -> proto.ArtifactCollectorArgs - 31, // 101: proto.API.GetClientMonitoringState:output_type -> proto.ClientEventTable - 19, // 102: proto.API.SetClientMonitoringState:output_type -> google.protobuf.Empty - 62, // 103: proto.API.ListAvailableEventResults:output_type -> proto.ListAvailableEventResultsResponse - 63, // 104: proto.API.CreateDownloadFile:output_type -> proto.CreateDownloadResponse - 64, // 105: proto.API.GetNotebooks:output_type -> proto.Notebooks - 35, // 106: proto.API.NewNotebook:output_type -> proto.NotebookMetadata - 35, // 107: proto.API.UpdateNotebook:output_type -> proto.NotebookMetadata - 35, // 108: proto.API.NewNotebookCell:output_type -> proto.NotebookMetadata - 65, // 109: proto.API.GetNotebookCell:output_type -> proto.NotebookCell - 65, // 110: proto.API.UpdateNotebookCell:output_type -> proto.NotebookCell - 19, // 111: proto.API.CancelNotebookCell:output_type -> google.protobuf.Empty - 19, // 112: proto.API.CreateNotebookDownloadFile:output_type -> google.protobuf.Empty - 66, // 113: proto.API.UploadNotebookAttachment:output_type -> proto.NotebookFileUploadResponse - 19, // 114: proto.API.ExportNotebook:output_type -> google.protobuf.Empty - 4, // 115: proto.API.VFSGetBuffer:output_type -> proto.VFSFileBuffer - 39, // 116: proto.API.Query:output_type -> proto.VQLResponse - 7, // 117: proto.API.WatchEvent:output_type -> proto.EventResponse - 19, // 118: proto.API.PushEvents:output_type -> google.protobuf.Empty - 19, // 119: proto.API.WriteEvent:output_type -> google.protobuf.Empty - 67, // 120: proto.API.GetSubject:output_type -> proto.DataResponse - 67, // 121: proto.API.SetSubject:output_type -> proto.DataResponse - 19, // 122: proto.API.DeleteSubject:output_type -> google.protobuf.Empty - 68, // 123: proto.API.ListChildren:output_type -> proto.ListChildrenResponse - 69, // 124: proto.API.Check:output_type -> proto.HealthCheckResponse - 63, // [63:125] is the sub-list for method output_type - 1, // [1:63] is the sub-list for method input_type + 10, // 2: proto.API.EstimateHunt:input_type -> proto.HuntEstimateRequest + 11, // 3: proto.API.GetHuntTable:input_type -> proto.GetTableRequest + 12, // 4: proto.API.ListHunts:input_type -> proto.ListHuntsRequest + 13, // 5: proto.API.GetHunt:input_type -> proto.GetHuntRequest + 14, // 6: proto.API.GetHuntTags:input_type -> google.protobuf.Empty + 15, // 7: proto.API.ModifyHunt:input_type -> proto.HuntMutation + 11, // 8: proto.API.GetHuntFlows:input_type -> proto.GetTableRequest + 16, // 9: proto.API.GetHuntResults:input_type -> proto.GetHuntResultsRequest + 5, // 10: proto.API.NotifyClients:input_type -> proto.NotificationRequest + 17, // 11: proto.API.LabelClients:input_type -> proto.LabelClientsRequest + 18, // 12: proto.API.ListClients:input_type -> proto.SearchClientsRequest + 19, // 13: proto.API.GetClient:input_type -> proto.GetClientRequest + 19, // 14: proto.API.GetClientMetadata:input_type -> proto.GetClientRequest + 20, // 15: proto.API.SetClientMetadata:input_type -> proto.SetClientMetadataRequest + 11, // 16: proto.API.GetClientFlows:input_type -> proto.GetTableRequest + 14, // 17: proto.API.GetUserUITraits:input_type -> google.protobuf.Empty + 21, // 18: proto.API.SetGUIOptions:input_type -> proto.SetGUIOptionsRequest + 14, // 19: proto.API.GetUsers:input_type -> google.protobuf.Empty + 14, // 20: proto.API.GetGlobalUsers:input_type -> google.protobuf.Empty + 22, // 21: proto.API.GetUserRoles:input_type -> proto.UserRequest + 23, // 22: proto.API.SetUserRoles:input_type -> proto.UserRoles + 22, // 23: proto.API.GetUser:input_type -> proto.UserRequest + 24, // 24: proto.API.CreateUser:input_type -> proto.UpdateUserRequest + 25, // 25: proto.API.GetUserFavorites:input_type -> proto.Favorite + 26, // 26: proto.API.SetPassword:input_type -> proto.SetPasswordRequest + 27, // 27: proto.API.VFSListDirectory:input_type -> proto.VFSListRequest + 11, // 28: proto.API.VFSListDirectoryFiles:input_type -> proto.GetTableRequest + 3, // 29: proto.API.VFSRefreshDirectory:input_type -> proto.VFSRefreshDirectoryRequest + 27, // 30: proto.API.VFSStatDirectory:input_type -> proto.VFSListRequest + 28, // 31: proto.API.VFSStatDownload:input_type -> proto.VFSStatDownloadRequest + 28, // 32: proto.API.VFSDownloadFile:input_type -> proto.VFSStatDownloadRequest + 11, // 33: proto.API.GetTable:input_type -> proto.GetTableRequest + 29, // 34: proto.API.SearchFile:input_type -> proto.SearchFileRequest + 30, // 35: proto.API.CollectArtifact:input_type -> proto.ArtifactCollectorArgs + 31, // 36: proto.API.CancelFlow:input_type -> proto.ApiFlowRequest + 31, // 37: proto.API.ResumeFlow:input_type -> proto.ApiFlowRequest + 31, // 38: proto.API.GetFlowDetails:input_type -> proto.ApiFlowRequest + 31, // 39: proto.API.GetFlowRequests:input_type -> proto.ApiFlowRequest + 14, // 40: proto.API.GetKeywordCompletions:input_type -> google.protobuf.Empty + 32, // 41: proto.API.ReformatVQL:input_type -> proto.ReformatVQLMessage + 33, // 42: proto.API.GetArtifacts:input_type -> proto.GetArtifactsRequest + 34, // 43: proto.API.GetArtifactFile:input_type -> proto.GetArtifactRequest + 35, // 44: proto.API.SetArtifactFile:input_type -> proto.SetArtifactRequest + 36, // 45: proto.API.LoadArtifactPack:input_type -> proto.LoadArtifactPackRequest + 37, // 46: proto.API.SearchDocs:input_type -> proto.DocSearchRequest + 38, // 47: proto.API.GetToolInfo:input_type -> proto.Tool + 38, // 48: proto.API.SetToolInfo:input_type -> proto.Tool + 39, // 49: proto.API.GetReport:input_type -> proto.GetReportRequest + 14, // 50: proto.API.GetServerMonitoringState:input_type -> google.protobuf.Empty + 30, // 51: proto.API.SetServerMonitoringState:input_type -> proto.ArtifactCollectorArgs + 40, // 52: proto.API.GetClientMonitoringState:input_type -> proto.GetClientMonitoringStateRequest + 41, // 53: proto.API.SetClientMonitoringState:input_type -> proto.ClientEventTable + 42, // 54: proto.API.ListAvailableEventResults:input_type -> proto.ListAvailableEventResultsRequest + 43, // 55: proto.API.CreateDownloadFile:input_type -> proto.CreateDownloadRequest + 44, // 56: proto.API.GetNotebooks:input_type -> proto.NotebookCellRequest + 45, // 57: proto.API.NewNotebook:input_type -> proto.NotebookMetadata + 45, // 58: proto.API.UpdateNotebook:input_type -> proto.NotebookMetadata + 45, // 59: proto.API.DeleteNotebook:input_type -> proto.NotebookMetadata + 44, // 60: proto.API.NewNotebookCell:input_type -> proto.NotebookCellRequest + 44, // 61: proto.API.GetNotebookCell:input_type -> proto.NotebookCellRequest + 44, // 62: proto.API.UpdateNotebookCell:input_type -> proto.NotebookCellRequest + 44, // 63: proto.API.RevertNotebookCell:input_type -> proto.NotebookCellRequest + 44, // 64: proto.API.CancelNotebookCell:input_type -> proto.NotebookCellRequest + 46, // 65: proto.API.CreateNotebookDownloadFile:input_type -> proto.NotebookExportRequest + 47, // 66: proto.API.UploadNotebookAttachment:input_type -> proto.NotebookFileUploadRequest + 47, // 67: proto.API.RemoveNotebookAttachment:input_type -> proto.NotebookFileUploadRequest + 48, // 68: proto.API.AnnotateTimeline:input_type -> proto.AnnotationRequest + 14, // 69: proto.API.GetSecretDefinitions:input_type -> google.protobuf.Empty + 49, // 70: proto.API.AddSecret:input_type -> proto.Secret + 50, // 71: proto.API.ModifySecret:input_type -> proto.ModifySecretRequest + 49, // 72: proto.API.GetSecret:input_type -> proto.Secret + 4, // 73: proto.API.VFSGetBuffer:input_type -> proto.VFSFileBuffer + 51, // 74: proto.API.Query:input_type -> proto.VQLCollectorArgs + 6, // 75: proto.API.WatchEvent:input_type -> proto.EventRequest + 8, // 76: proto.API.PushEvents:input_type -> proto.PushEventRequest + 52, // 77: proto.API.WriteEvent:input_type -> proto.VQLResponse + 53, // 78: proto.API.Scheduler:input_type -> proto.ScheduleRequest + 54, // 79: proto.API.GetSubject:input_type -> proto.DataRequest + 54, // 80: proto.API.SetSubject:input_type -> proto.DataRequest + 54, // 81: proto.API.DeleteSubject:input_type -> proto.DataRequest + 54, // 82: proto.API.ListChildren:input_type -> proto.DataRequest + 55, // 83: proto.API.Check:input_type -> proto.HealthCheckRequest + 0, // 84: proto.API.CreateHunt:output_type -> proto.StartFlowResponse + 56, // 85: proto.API.EstimateHunt:output_type -> proto.HuntStats + 57, // 86: proto.API.GetHuntTable:output_type -> proto.GetTableResponse + 58, // 87: proto.API.ListHunts:output_type -> proto.ListHuntsResponse + 9, // 88: proto.API.GetHunt:output_type -> proto.Hunt + 59, // 89: proto.API.GetHuntTags:output_type -> proto.HuntTags + 14, // 90: proto.API.ModifyHunt:output_type -> google.protobuf.Empty + 57, // 91: proto.API.GetHuntFlows:output_type -> proto.GetTableResponse + 57, // 92: proto.API.GetHuntResults:output_type -> proto.GetTableResponse + 14, // 93: proto.API.NotifyClients:output_type -> google.protobuf.Empty + 60, // 94: proto.API.LabelClients:output_type -> proto.APIResponse + 61, // 95: proto.API.ListClients:output_type -> proto.SearchClientsResponse + 62, // 96: proto.API.GetClient:output_type -> proto.ApiClient + 63, // 97: proto.API.GetClientMetadata:output_type -> proto.ClientMetadata + 14, // 98: proto.API.SetClientMetadata:output_type -> google.protobuf.Empty + 57, // 99: proto.API.GetClientFlows:output_type -> proto.GetTableResponse + 64, // 100: proto.API.GetUserUITraits:output_type -> proto.ApiUser + 65, // 101: proto.API.SetGUIOptions:output_type -> proto.SetGUIOptionsResponse + 66, // 102: proto.API.GetUsers:output_type -> proto.Users + 66, // 103: proto.API.GetGlobalUsers:output_type -> proto.Users + 23, // 104: proto.API.GetUserRoles:output_type -> proto.UserRoles + 14, // 105: proto.API.SetUserRoles:output_type -> google.protobuf.Empty + 67, // 106: proto.API.GetUser:output_type -> proto.VelociraptorUser + 14, // 107: proto.API.CreateUser:output_type -> google.protobuf.Empty + 68, // 108: proto.API.GetUserFavorites:output_type -> proto.Favorites + 14, // 109: proto.API.SetPassword:output_type -> google.protobuf.Empty + 69, // 110: proto.API.VFSListDirectory:output_type -> proto.VFSListResponse + 57, // 111: proto.API.VFSListDirectoryFiles:output_type -> proto.GetTableResponse + 70, // 112: proto.API.VFSRefreshDirectory:output_type -> proto.ArtifactCollectorResponse + 69, // 113: proto.API.VFSStatDirectory:output_type -> proto.VFSListResponse + 71, // 114: proto.API.VFSStatDownload:output_type -> proto.VFSDownloadInfo + 0, // 115: proto.API.VFSDownloadFile:output_type -> proto.StartFlowResponse + 57, // 116: proto.API.GetTable:output_type -> proto.GetTableResponse + 72, // 117: proto.API.SearchFile:output_type -> proto.SearchFileResponse + 70, // 118: proto.API.CollectArtifact:output_type -> proto.ArtifactCollectorResponse + 0, // 119: proto.API.CancelFlow:output_type -> proto.StartFlowResponse + 14, // 120: proto.API.ResumeFlow:output_type -> google.protobuf.Empty + 73, // 121: proto.API.GetFlowDetails:output_type -> proto.FlowDetails + 74, // 122: proto.API.GetFlowRequests:output_type -> proto.ApiFlowRequestDetails + 75, // 123: proto.API.GetKeywordCompletions:output_type -> proto.KeywordCompletions + 32, // 124: proto.API.ReformatVQL:output_type -> proto.ReformatVQLMessage + 76, // 125: proto.API.GetArtifacts:output_type -> proto.ArtifactDescriptors + 77, // 126: proto.API.GetArtifactFile:output_type -> proto.GetArtifactResponse + 78, // 127: proto.API.SetArtifactFile:output_type -> proto.SetArtifactResponse + 79, // 128: proto.API.LoadArtifactPack:output_type -> proto.LoadArtifactPackResponse + 80, // 129: proto.API.SearchDocs:output_type -> proto.DocSearchResponses + 38, // 130: proto.API.GetToolInfo:output_type -> proto.Tool + 38, // 131: proto.API.SetToolInfo:output_type -> proto.Tool + 81, // 132: proto.API.GetReport:output_type -> proto.GetReportResponse + 30, // 133: proto.API.GetServerMonitoringState:output_type -> proto.ArtifactCollectorArgs + 30, // 134: proto.API.SetServerMonitoringState:output_type -> proto.ArtifactCollectorArgs + 41, // 135: proto.API.GetClientMonitoringState:output_type -> proto.ClientEventTable + 14, // 136: proto.API.SetClientMonitoringState:output_type -> google.protobuf.Empty + 82, // 137: proto.API.ListAvailableEventResults:output_type -> proto.ListAvailableEventResultsResponse + 83, // 138: proto.API.CreateDownloadFile:output_type -> proto.CreateDownloadResponse + 84, // 139: proto.API.GetNotebooks:output_type -> proto.Notebooks + 45, // 140: proto.API.NewNotebook:output_type -> proto.NotebookMetadata + 45, // 141: proto.API.UpdateNotebook:output_type -> proto.NotebookMetadata + 14, // 142: proto.API.DeleteNotebook:output_type -> google.protobuf.Empty + 45, // 143: proto.API.NewNotebookCell:output_type -> proto.NotebookMetadata + 85, // 144: proto.API.GetNotebookCell:output_type -> proto.NotebookCell + 85, // 145: proto.API.UpdateNotebookCell:output_type -> proto.NotebookCell + 85, // 146: proto.API.RevertNotebookCell:output_type -> proto.NotebookCell + 14, // 147: proto.API.CancelNotebookCell:output_type -> google.protobuf.Empty + 14, // 148: proto.API.CreateNotebookDownloadFile:output_type -> google.protobuf.Empty + 86, // 149: proto.API.UploadNotebookAttachment:output_type -> proto.NotebookFileUploadResponse + 14, // 150: proto.API.RemoveNotebookAttachment:output_type -> google.protobuf.Empty + 14, // 151: proto.API.AnnotateTimeline:output_type -> google.protobuf.Empty + 87, // 152: proto.API.GetSecretDefinitions:output_type -> proto.SecretDefinitionList + 14, // 153: proto.API.AddSecret:output_type -> google.protobuf.Empty + 14, // 154: proto.API.ModifySecret:output_type -> google.protobuf.Empty + 49, // 155: proto.API.GetSecret:output_type -> proto.Secret + 4, // 156: proto.API.VFSGetBuffer:output_type -> proto.VFSFileBuffer + 52, // 157: proto.API.Query:output_type -> proto.VQLResponse + 7, // 158: proto.API.WatchEvent:output_type -> proto.EventResponse + 14, // 159: proto.API.PushEvents:output_type -> google.protobuf.Empty + 14, // 160: proto.API.WriteEvent:output_type -> google.protobuf.Empty + 88, // 161: proto.API.Scheduler:output_type -> proto.ScheduleResponse + 89, // 162: proto.API.GetSubject:output_type -> proto.DataResponse + 89, // 163: proto.API.SetSubject:output_type -> proto.DataResponse + 14, // 164: proto.API.DeleteSubject:output_type -> google.protobuf.Empty + 90, // 165: proto.API.ListChildren:output_type -> proto.ListChildrenResponse + 91, // 166: proto.API.Check:output_type -> proto.HealthCheckResponse + 84, // [84:167] is the sub-list for method output_type + 1, // [1:84] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name 1, // [1:1] is the sub-list for extension extendee 0, // [0:1] is the sub-list for field type_name @@ -1252,6 +1514,7 @@ func file_api_proto_init() { file_artifacts_proto_init() file_clients_proto_init() file_datastore_proto_init() + file_docs_proto_init() file_health_proto_init() file_hunts_proto_init() file_flows_proto_init() @@ -1261,6 +1524,9 @@ func file_api_proto_init() { file_download_proto_init() file_completions_proto_init() file_vfs_api_proto_init() + file_scheduler_proto_init() + file_secrets_proto_init() + file_timeline_api_proto_init() if !protoimpl.UnsafeEnabled { file_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*StartFlowResponse); i { @@ -1371,6 +1637,7 @@ func file_api_proto_init() { } } } + file_api_proto_msgTypes[4].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/api/proto/api.pb.gw.go b/api/proto/api.pb.gw.go index eea7af381..0f6fecb85 100644 --- a/api/proto/api.pb.gw.go +++ b/api/proto/api.pb.gw.go @@ -13,7 +13,6 @@ import ( "io" "net/http" - "github.com/golang/protobuf/ptypes/empty" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" "google.golang.org/grpc" @@ -22,8 +21,9 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" - proto_4 "www.velocidex.com/golang/velociraptor/artifacts/proto" - proto_2 "www.velocidex.com/golang/velociraptor/flows/proto" + "google.golang.org/protobuf/types/known/emptypb" + proto_5 "www.velocidex.com/golang/velociraptor/artifacts/proto" + proto_0 "www.velocidex.com/golang/velociraptor/flows/proto" ) // Suppress "imported and not used" errors @@ -69,7 +69,7 @@ func local_request_API_CreateHunt_0(ctx context.Context, marshaler runtime.Marsh } func request_API_EstimateHunt_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq Hunt + var protoReq HuntEstimateRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -86,7 +86,7 @@ func request_API_EstimateHunt_0(ctx context.Context, marshaler runtime.Marshaler } func local_request_API_EstimateHunt_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq Hunt + var protoReq HuntEstimateRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -102,6 +102,42 @@ func local_request_API_EstimateHunt_0(ctx context.Context, marshaler runtime.Mar } +var ( + filter_API_GetHuntTable_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_API_GetHuntTable_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetTableRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetHuntTable_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetHuntTable(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_GetHuntTable_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetTableRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetHuntTable_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetHuntTable(ctx, &protoReq) + return msg, metadata, err + +} + var ( filter_API_ListHunts_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) @@ -174,8 +210,26 @@ func local_request_API_GetHunt_0(ctx context.Context, marshaler runtime.Marshale } +func request_API_GetHuntTags_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := client.GetHuntTags(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_GetHuntTags_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := server.GetHuntTags(ctx, &protoReq) + return msg, metadata, err + +} + func request_API_ModifyHunt_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq Hunt + var protoReq HuntMutation var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -192,7 +246,7 @@ func request_API_ModifyHunt_0(ctx context.Context, marshaler runtime.Marshaler, } func local_request_API_ModifyHunt_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq Hunt + var protoReq HuntMutation var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -525,7 +579,7 @@ func local_request_API_GetClientMetadata_0(ctx context.Context, marshaler runtim } func request_API_SetClientMetadata_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ClientMetadata + var protoReq SetClientMetadataRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -542,7 +596,7 @@ func request_API_SetClientMetadata_0(ctx context.Context, marshaler runtime.Mars } func local_request_API_SetClientMetadata_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ClientMetadata + var protoReq SetClientMetadataRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -559,30 +613,13 @@ func local_request_API_SetClientMetadata_0(ctx context.Context, marshaler runtim } var ( - filter_API_GetClientFlows_0 = &utilities.DoubleArray{Encoding: map[string]int{"client_id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + filter_API_GetClientFlows_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) func request_API_GetClientFlows_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest + var protoReq GetTableRequest var metadata runtime.ServerMetadata - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["client_id"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "client_id") - } - - protoReq.ClientId, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "client_id", err) - } - if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -596,26 +633,9 @@ func request_API_GetClientFlows_0(ctx context.Context, marshaler runtime.Marshal } func local_request_API_GetClientFlows_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest + var protoReq GetTableRequest var metadata runtime.ServerMetadata - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["client_id"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "client_id") - } - - protoReq.ClientId, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "client_id", err) - } - if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -628,78 +648,8 @@ func local_request_API_GetClientFlows_0(ctx context.Context, marshaler runtime.M } -var ( - filter_API_GetClientFlows_1 = &utilities.DoubleArray{Encoding: map[string]int{"client_id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} -) - -func request_API_GetClientFlows_1(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest - var metadata runtime.ServerMetadata - - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["client_id"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "client_id") - } - - protoReq.ClientId, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "client_id", err) - } - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetClientFlows_1); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.GetClientFlows(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_API_GetClientFlows_1(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest - var metadata runtime.ServerMetadata - - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["client_id"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "client_id") - } - - protoReq.ClientId, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "client_id", err) - } - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetClientFlows_1); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.GetClientFlows(ctx, &protoReq) - return msg, metadata, err - -} - func request_API_GetUserUITraits_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq empty.Empty + var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := client.GetUserUITraits(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) @@ -708,7 +658,7 @@ func request_API_GetUserUITraits_0(ctx context.Context, marshaler runtime.Marsha } func local_request_API_GetUserUITraits_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq empty.Empty + var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := server.GetUserUITraits(ctx, &protoReq) @@ -751,7 +701,7 @@ func local_request_API_SetGUIOptions_0(ctx context.Context, marshaler runtime.Ma } func request_API_GetUsers_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq empty.Empty + var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := client.GetUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) @@ -760,7 +710,7 @@ func request_API_GetUsers_0(ctx context.Context, marshaler runtime.Marshaler, cl } func local_request_API_GetUsers_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq empty.Empty + var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := server.GetUsers(ctx, &protoReq) @@ -768,48 +718,100 @@ func local_request_API_GetUsers_0(ctx context.Context, marshaler runtime.Marshal } +func request_API_GetGlobalUsers_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := client.GetGlobalUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_GetGlobalUsers_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := server.GetGlobalUsers(ctx, &protoReq) + return msg, metadata, err + +} + var ( - filter_API_GetUserFavorites_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_API_GetUserRoles_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) -func request_API_GetUserFavorites_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq Favorite +func request_API_GetUserRoles_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UserRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetUserFavorites_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetUserRoles_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.GetUserFavorites(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.GetUserRoles(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_GetUserFavorites_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq Favorite +func local_request_API_GetUserRoles_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UserRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetUserFavorites_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetUserRoles_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetUserFavorites(ctx, &protoReq) + msg, err := server.GetUserRoles(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_SetUserRoles_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UserRoles + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.SetUserRoles(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_SetUserRoles_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UserRoles + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.SetUserRoles(ctx, &protoReq) return msg, metadata, err } var ( - filter_API_VFSListDirectory_0 = &utilities.DoubleArray{Encoding: map[string]int{"client_id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + filter_API_GetUser_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} ) -func request_API_VFSListDirectory_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq VFSListRequest +func request_API_GetUser_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UserRequest var metadata runtime.ServerMetadata var ( @@ -819,30 +821,30 @@ func request_API_VFSListDirectory_0(ctx context.Context, marshaler runtime.Marsh _ = err ) - val, ok = pathParams["client_id"] + val, ok = pathParams["name"] if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "client_id") + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") } - protoReq.ClientId, err = runtime.String(val) + protoReq.Name, err = runtime.String(val) if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "client_id", err) + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSListDirectory_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetUser_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.VFSListDirectory(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.GetUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_VFSListDirectory_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq VFSListRequest +func local_request_API_GetUser_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UserRequest var metadata runtime.ServerMetadata var ( @@ -852,30 +854,30 @@ func local_request_API_VFSListDirectory_0(ctx context.Context, marshaler runtime _ = err ) - val, ok = pathParams["client_id"] + val, ok = pathParams["name"] if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "client_id") + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") } - protoReq.ClientId, err = runtime.String(val) + protoReq.Name, err = runtime.String(val) if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "client_id", err) + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSListDirectory_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetUser_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.VFSListDirectory(ctx, &protoReq) + msg, err := server.GetUser(ctx, &protoReq) return msg, metadata, err } -func request_API_VFSRefreshDirectory_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq VFSRefreshDirectoryRequest +func request_API_CreateUser_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UpdateUserRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -886,13 +888,13 @@ func request_API_VFSRefreshDirectory_0(ctx context.Context, marshaler runtime.Ma return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.VFSRefreshDirectory(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.CreateUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_VFSRefreshDirectory_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq VFSRefreshDirectoryRequest +func local_request_API_CreateUser_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UpdateUserRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -903,189 +905,189 @@ func local_request_API_VFSRefreshDirectory_0(ctx context.Context, marshaler runt return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.VFSRefreshDirectory(ctx, &protoReq) + msg, err := server.CreateUser(ctx, &protoReq) return msg, metadata, err } var ( - filter_API_VFSStatDirectory_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_API_GetUserFavorites_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) -func request_API_VFSStatDirectory_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq VFSListRequest +func request_API_GetUserFavorites_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Favorite var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSStatDirectory_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetUserFavorites_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.VFSStatDirectory(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.GetUserFavorites(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_VFSStatDirectory_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq VFSListRequest +func local_request_API_GetUserFavorites_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Favorite var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSStatDirectory_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetUserFavorites_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.VFSStatDirectory(ctx, &protoReq) + msg, err := server.GetUserFavorites(ctx, &protoReq) return msg, metadata, err } -var ( - filter_API_VFSStatDownload_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} -) - -func request_API_VFSStatDownload_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq VFSStatDownloadRequest +func request_API_SetPassword_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SetPasswordRequest var metadata runtime.ServerMetadata - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSStatDownload_0); err != nil { + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.VFSStatDownload(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.SetPassword(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_VFSStatDownload_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq VFSStatDownloadRequest +func local_request_API_SetPassword_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SetPasswordRequest var metadata runtime.ServerMetadata - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSStatDownload_0); err != nil { + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.VFSStatDownload(ctx, &protoReq) + msg, err := server.SetPassword(ctx, &protoReq) return msg, metadata, err } var ( - filter_API_GetTable_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_API_VFSListDirectory_0 = &utilities.DoubleArray{Encoding: map[string]int{"client_id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} ) -func request_API_GetTable_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetTableRequest +func request_API_VFSListDirectory_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq VFSListRequest var metadata runtime.ServerMetadata - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetTable_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.GetTable(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err + var ( + val string + ok bool + err error + _ = err + ) -} + val, ok = pathParams["client_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "client_id") + } -func local_request_API_GetTable_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetTableRequest - var metadata runtime.ServerMetadata + protoReq.ClientId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "client_id", err) + } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetTable_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSListDirectory_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetTable(ctx, &protoReq) + msg, err := client.VFSListDirectory(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func request_API_CollectArtifact_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_2.ArtifactCollectorArgs +func local_request_API_VFSListDirectory_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq VFSListRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.CollectArtifact(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err + var ( + val string + ok bool + err error + _ = err + ) -} + val, ok = pathParams["client_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "client_id") + } -func local_request_API_CollectArtifact_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_2.ArtifactCollectorArgs - var metadata runtime.ServerMetadata + protoReq.ClientId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "client_id", err) + } - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSListDirectory_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.CollectArtifact(ctx, &protoReq) + msg, err := server.VFSListDirectory(ctx, &protoReq) return msg, metadata, err } -func request_API_CancelFlow_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest +var ( + filter_API_VFSListDirectoryFiles_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_API_VFSListDirectoryFiles_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetTableRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSListDirectoryFiles_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.CancelFlow(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.VFSListDirectoryFiles(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_CancelFlow_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest +func local_request_API_VFSListDirectoryFiles_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetTableRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSListDirectoryFiles_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.CancelFlow(ctx, &protoReq) + msg, err := server.VFSListDirectoryFiles(ctx, &protoReq) return msg, metadata, err } -func request_API_ArchiveFlow_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest +func request_API_VFSRefreshDirectory_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq VFSRefreshDirectoryRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1096,13 +1098,13 @@ func request_API_ArchiveFlow_0(ctx context.Context, marshaler runtime.Marshaler, return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.ArchiveFlow(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.VFSRefreshDirectory(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_ArchiveFlow_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest +func local_request_API_VFSRefreshDirectory_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq VFSRefreshDirectoryRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1113,103 +1115,85 @@ func local_request_API_ArchiveFlow_0(ctx context.Context, marshaler runtime.Mars return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.ArchiveFlow(ctx, &protoReq) + msg, err := server.VFSRefreshDirectory(ctx, &protoReq) return msg, metadata, err } var ( - filter_API_GetFlowDetails_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_API_VFSStatDirectory_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) -func request_API_GetFlowDetails_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest +func request_API_VFSStatDirectory_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq VFSListRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetFlowDetails_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSStatDirectory_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.GetFlowDetails(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.VFSStatDirectory(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_GetFlowDetails_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest +func local_request_API_VFSStatDirectory_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq VFSListRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetFlowDetails_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSStatDirectory_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetFlowDetails(ctx, &protoReq) + msg, err := server.VFSStatDirectory(ctx, &protoReq) return msg, metadata, err } var ( - filter_API_GetFlowRequests_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_API_VFSStatDownload_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) -func request_API_GetFlowRequests_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest +func request_API_VFSStatDownload_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq VFSStatDownloadRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetFlowRequests_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSStatDownload_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.GetFlowRequests(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.VFSStatDownload(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_GetFlowRequests_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ApiFlowRequest +func local_request_API_VFSStatDownload_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq VFSStatDownloadRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetFlowRequests_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_VFSStatDownload_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetFlowRequests(ctx, &protoReq) - return msg, metadata, err - -} - -func request_API_GetKeywordCompletions_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq empty.Empty - var metadata runtime.ServerMetadata - - msg, err := client.GetKeywordCompletions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_API_GetKeywordCompletions_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq empty.Empty - var metadata runtime.ServerMetadata - - msg, err := server.GetKeywordCompletions(ctx, &protoReq) + msg, err := server.VFSStatDownload(ctx, &protoReq) return msg, metadata, err } -func request_API_GetArtifacts_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetArtifactsRequest +func request_API_VFSDownloadFile_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq VFSStatDownloadRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1220,13 +1204,13 @@ func request_API_GetArtifacts_0(ctx context.Context, marshaler runtime.Marshaler return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.GetArtifacts(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.VFSDownloadFile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_GetArtifacts_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetArtifactsRequest +func local_request_API_VFSDownloadFile_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq VFSStatDownloadRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1237,49 +1221,49 @@ func local_request_API_GetArtifacts_0(ctx context.Context, marshaler runtime.Mar return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetArtifacts(ctx, &protoReq) + msg, err := server.VFSDownloadFile(ctx, &protoReq) return msg, metadata, err } var ( - filter_API_GetArtifactFile_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_API_GetTable_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) -func request_API_GetArtifactFile_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetArtifactRequest +func request_API_GetTable_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetTableRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetArtifactFile_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetTable_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.GetArtifactFile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.GetTable(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_GetArtifactFile_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetArtifactRequest +func local_request_API_GetTable_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetTableRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetArtifactFile_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetTable_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetArtifactFile(ctx, &protoReq) + msg, err := server.GetTable(ctx, &protoReq) return msg, metadata, err } -func request_API_SetArtifactFile_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq SetArtifactRequest +func request_API_SearchFile_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SearchFileRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1290,13 +1274,13 @@ func request_API_SetArtifactFile_0(ctx context.Context, marshaler runtime.Marsha return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.SetArtifactFile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.SearchFile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_SetArtifactFile_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq SetArtifactRequest +func local_request_API_SearchFile_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SearchFileRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1307,13 +1291,13 @@ func local_request_API_SetArtifactFile_0(ctx context.Context, marshaler runtime. return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.SetArtifactFile(ctx, &protoReq) + msg, err := server.SearchFile(ctx, &protoReq) return msg, metadata, err } -func request_API_LoadArtifactPack_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq VFSFileBuffer +func request_API_CollectArtifact_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_0.ArtifactCollectorArgs var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1324,13 +1308,13 @@ func request_API_LoadArtifactPack_0(ctx context.Context, marshaler runtime.Marsh return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.LoadArtifactPack(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.CollectArtifact(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_LoadArtifactPack_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq VFSFileBuffer +func local_request_API_CollectArtifact_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_0.ArtifactCollectorArgs var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1341,49 +1325,47 @@ func local_request_API_LoadArtifactPack_0(ctx context.Context, marshaler runtime return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.LoadArtifactPack(ctx, &protoReq) + msg, err := server.CollectArtifact(ctx, &protoReq) return msg, metadata, err } -var ( - filter_API_GetToolInfo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} -) - -func request_API_GetToolInfo_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_4.Tool +func request_API_CancelFlow_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ApiFlowRequest var metadata runtime.ServerMetadata - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetToolInfo_0); err != nil { + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.GetToolInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.CancelFlow(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_GetToolInfo_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_4.Tool +func local_request_API_CancelFlow_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ApiFlowRequest var metadata runtime.ServerMetadata - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetToolInfo_0); err != nil { + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetToolInfo(ctx, &protoReq) + msg, err := server.CancelFlow(ctx, &protoReq) return msg, metadata, err } -func request_API_SetToolInfo_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_4.Tool +func request_API_ResumeFlow_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ApiFlowRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1394,13 +1376,13 @@ func request_API_SetToolInfo_0(ctx context.Context, marshaler runtime.Marshaler, return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.SetToolInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.ResumeFlow(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_SetToolInfo_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_4.Tool +func local_request_API_ResumeFlow_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ApiFlowRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1411,65 +1393,103 @@ func local_request_API_SetToolInfo_0(ctx context.Context, marshaler runtime.Mars return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.SetToolInfo(ctx, &protoReq) + msg, err := server.ResumeFlow(ctx, &protoReq) return msg, metadata, err } -func request_API_GetReport_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetReportRequest +var ( + filter_API_GetFlowDetails_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_API_GetFlowDetails_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ApiFlowRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetFlowDetails_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.GetReport(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.GetFlowDetails(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_GetReport_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetReportRequest +func local_request_API_GetFlowDetails_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ApiFlowRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetFlowDetails_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetReport(ctx, &protoReq) + msg, err := server.GetFlowDetails(ctx, &protoReq) return msg, metadata, err } -func request_API_GetServerMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq empty.Empty +var ( + filter_API_GetFlowRequests_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_API_GetFlowRequests_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ApiFlowRequest var metadata runtime.ServerMetadata - msg, err := client.GetServerMonitoringState(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetFlowRequests_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetFlowRequests(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_GetServerMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq empty.Empty +func local_request_API_GetFlowRequests_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ApiFlowRequest var metadata runtime.ServerMetadata - msg, err := server.GetServerMonitoringState(ctx, &protoReq) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetFlowRequests_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetFlowRequests(ctx, &protoReq) return msg, metadata, err } -func request_API_SetServerMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_2.ArtifactCollectorArgs +func request_API_GetKeywordCompletions_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := client.GetKeywordCompletions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_GetKeywordCompletions_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := server.GetKeywordCompletions(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_ReformatVQL_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ReformatVQLMessage var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1480,13 +1500,13 @@ func request_API_SetServerMonitoringState_0(ctx context.Context, marshaler runti return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.SetServerMonitoringState(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.ReformatVQL(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_SetServerMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_2.ArtifactCollectorArgs +func local_request_API_ReformatVQL_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ReformatVQLMessage var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1497,49 +1517,83 @@ func local_request_API_SetServerMonitoringState_0(ctx context.Context, marshaler return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.SetServerMonitoringState(ctx, &protoReq) + msg, err := server.ReformatVQL(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_GetArtifacts_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetArtifactsRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetArtifacts(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_GetArtifacts_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetArtifactsRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetArtifacts(ctx, &protoReq) return msg, metadata, err } var ( - filter_API_GetClientMonitoringState_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_API_GetArtifactFile_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) -func request_API_GetClientMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_2.GetClientMonitoringStateRequest +func request_API_GetArtifactFile_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetArtifactRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetClientMonitoringState_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetArtifactFile_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.GetClientMonitoringState(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.GetArtifactFile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_GetClientMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_2.GetClientMonitoringStateRequest +func local_request_API_GetArtifactFile_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetArtifactRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetClientMonitoringState_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetArtifactFile_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetClientMonitoringState(ctx, &protoReq) + msg, err := server.GetArtifactFile(ctx, &protoReq) return msg, metadata, err } -func request_API_SetClientMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_2.ClientEventTable +func request_API_SetArtifactFile_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SetArtifactRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1550,13 +1604,13 @@ func request_API_SetClientMonitoringState_0(ctx context.Context, marshaler runti return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.SetClientMonitoringState(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.SetArtifactFile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_SetClientMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq proto_2.ClientEventTable +func local_request_API_SetArtifactFile_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SetArtifactRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1567,13 +1621,13 @@ func local_request_API_SetClientMonitoringState_0(ctx context.Context, marshaler return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.SetClientMonitoringState(ctx, &protoReq) + msg, err := server.SetArtifactFile(ctx, &protoReq) return msg, metadata, err } -func request_API_ListAvailableEventResults_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ListAvailableEventResultsRequest +func request_API_LoadArtifactPack_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq LoadArtifactPackRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1584,13 +1638,13 @@ func request_API_ListAvailableEventResults_0(ctx context.Context, marshaler runt return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.ListAvailableEventResults(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.LoadArtifactPack(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_ListAvailableEventResults_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ListAvailableEventResultsRequest +func local_request_API_LoadArtifactPack_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq LoadArtifactPackRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1601,83 +1655,85 @@ func local_request_API_ListAvailableEventResults_0(ctx context.Context, marshale return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.ListAvailableEventResults(ctx, &protoReq) + msg, err := server.LoadArtifactPack(ctx, &protoReq) return msg, metadata, err } -func request_API_CreateDownloadFile_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq CreateDownloadRequest +var ( + filter_API_SearchDocs_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_API_SearchDocs_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DocSearchRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_SearchDocs_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.CreateDownloadFile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.SearchDocs(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_CreateDownloadFile_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq CreateDownloadRequest +func local_request_API_SearchDocs_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DocSearchRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_SearchDocs_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.CreateDownloadFile(ctx, &protoReq) + msg, err := server.SearchDocs(ctx, &protoReq) return msg, metadata, err } var ( - filter_API_GetNotebooks_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_API_GetToolInfo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) -func request_API_GetNotebooks_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookCellRequest +func request_API_GetToolInfo_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_5.Tool var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetNotebooks_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetToolInfo_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.GetNotebooks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.GetToolInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_GetNotebooks_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookCellRequest +func local_request_API_GetToolInfo_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_5.Tool var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetNotebooks_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetToolInfo_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetNotebooks(ctx, &protoReq) + msg, err := server.GetToolInfo(ctx, &protoReq) return msg, metadata, err } -func request_API_NewNotebook_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookMetadata +func request_API_SetToolInfo_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_5.Tool var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1688,13 +1744,13 @@ func request_API_NewNotebook_0(ctx context.Context, marshaler runtime.Marshaler, return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.NewNotebook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.SetToolInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_NewNotebook_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookMetadata +func local_request_API_SetToolInfo_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_5.Tool var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1705,13 +1761,13 @@ func local_request_API_NewNotebook_0(ctx context.Context, marshaler runtime.Mars return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.NewNotebook(ctx, &protoReq) + msg, err := server.SetToolInfo(ctx, &protoReq) return msg, metadata, err } -func request_API_UpdateNotebook_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookMetadata +func request_API_GetReport_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetReportRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1722,13 +1778,13 @@ func request_API_UpdateNotebook_0(ctx context.Context, marshaler runtime.Marshal return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.UpdateNotebook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.GetReport(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_UpdateNotebook_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookMetadata +func local_request_API_GetReport_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetReportRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1739,13 +1795,31 @@ func local_request_API_UpdateNotebook_0(ctx context.Context, marshaler runtime.M return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.UpdateNotebook(ctx, &protoReq) + msg, err := server.GetReport(ctx, &protoReq) return msg, metadata, err } -func request_API_NewNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookCellRequest +func request_API_GetServerMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := client.GetServerMonitoringState(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_GetServerMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := server.GetServerMonitoringState(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_SetServerMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_0.ArtifactCollectorArgs var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1756,13 +1830,13 @@ func request_API_NewNotebookCell_0(ctx context.Context, marshaler runtime.Marsha return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.NewNotebookCell(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.SetServerMonitoringState(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_NewNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookCellRequest +func local_request_API_SetServerMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_0.ArtifactCollectorArgs var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1773,49 +1847,49 @@ func local_request_API_NewNotebookCell_0(ctx context.Context, marshaler runtime. return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.NewNotebookCell(ctx, &protoReq) + msg, err := server.SetServerMonitoringState(ctx, &protoReq) return msg, metadata, err } var ( - filter_API_GetNotebookCell_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_API_GetClientMonitoringState_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) -func request_API_GetNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookCellRequest +func request_API_GetClientMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_0.GetClientMonitoringStateRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetNotebookCell_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetClientMonitoringState_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.GetNotebookCell(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.GetClientMonitoringState(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_GetNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookCellRequest +func local_request_API_GetClientMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_0.GetClientMonitoringStateRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetNotebookCell_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetClientMonitoringState_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetNotebookCell(ctx, &protoReq) + msg, err := server.GetClientMonitoringState(ctx, &protoReq) return msg, metadata, err } -func request_API_UpdateNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookCellRequest +func request_API_SetClientMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_0.ClientEventTable var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1826,13 +1900,13 @@ func request_API_UpdateNotebookCell_0(ctx context.Context, marshaler runtime.Mar return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.UpdateNotebookCell(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.SetClientMonitoringState(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_UpdateNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookCellRequest +func local_request_API_SetClientMonitoringState_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq proto_0.ClientEventTable var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1843,13 +1917,13 @@ func local_request_API_UpdateNotebookCell_0(ctx context.Context, marshaler runti return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.UpdateNotebookCell(ctx, &protoReq) + msg, err := server.SetClientMonitoringState(ctx, &protoReq) return msg, metadata, err } -func request_API_CancelNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookCellRequest +func request_API_ListAvailableEventResults_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListAvailableEventResultsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1860,13 +1934,13 @@ func request_API_CancelNotebookCell_0(ctx context.Context, marshaler runtime.Mar return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.CancelNotebookCell(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.ListAvailableEventResults(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_CancelNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookCellRequest +func local_request_API_ListAvailableEventResults_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListAvailableEventResultsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1877,13 +1951,13 @@ func local_request_API_CancelNotebookCell_0(ctx context.Context, marshaler runti return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.CancelNotebookCell(ctx, &protoReq) + msg, err := server.ListAvailableEventResults(ctx, &protoReq) return msg, metadata, err } -func request_API_CreateNotebookDownloadFile_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookExportRequest +func request_API_CreateDownloadFile_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CreateDownloadRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1894,13 +1968,13 @@ func request_API_CreateNotebookDownloadFile_0(ctx context.Context, marshaler run return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.CreateNotebookDownloadFile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.CreateDownloadFile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_CreateNotebookDownloadFile_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookExportRequest +func local_request_API_CreateDownloadFile_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CreateDownloadRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1911,13 +1985,49 @@ func local_request_API_CreateNotebookDownloadFile_0(ctx context.Context, marshal return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.CreateNotebookDownloadFile(ctx, &protoReq) + msg, err := server.CreateDownloadFile(ctx, &protoReq) return msg, metadata, err } -func request_API_UploadNotebookAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookFileUploadRequest +var ( + filter_API_GetNotebooks_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_API_GetNotebooks_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetNotebooks_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetNotebooks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_GetNotebooks_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetNotebooks_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetNotebooks(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_NewNotebook_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookMetadata var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1928,13 +2038,13 @@ func request_API_UploadNotebookAttachment_0(ctx context.Context, marshaler runti return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.UploadNotebookAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.NewNotebook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_UploadNotebookAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookFileUploadRequest +func local_request_API_NewNotebook_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookMetadata var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1945,13 +2055,13 @@ func local_request_API_UploadNotebookAttachment_0(ctx context.Context, marshaler return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.UploadNotebookAttachment(ctx, &protoReq) + msg, err := server.NewNotebook(ctx, &protoReq) return msg, metadata, err } -func request_API_ExportNotebook_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookExportRequest +func request_API_UpdateNotebook_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookMetadata var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1962,13 +2072,202 @@ func request_API_ExportNotebook_0(ctx context.Context, marshaler runtime.Marshal return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.ExportNotebook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.UpdateNotebook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_API_ExportNotebook_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq NotebookExportRequest +func local_request_API_UpdateNotebook_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookMetadata + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.UpdateNotebook(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_DeleteNotebook_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookMetadata + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.DeleteNotebook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_DeleteNotebook_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookMetadata + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.DeleteNotebook(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_NewNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.NewNotebookCell(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_NewNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.NewNotebookCell(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_API_GetNotebookCell_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_API_GetNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetNotebookCell_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetNotebookCell(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_GetNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetNotebookCell_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetNotebookCell(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_UpdateNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.UpdateNotebookCell(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_UpdateNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.UpdateNotebookCell(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_RevertNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.RevertNotebookCell(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_RevertNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.RevertNotebookCell(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_CancelNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) @@ -1979,29 +2278,741 @@ func local_request_API_ExportNotebook_0(ctx context.Context, marshaler runtime.M return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.ExportNotebook(ctx, &protoReq) - return msg, metadata, err + msg, err := client.CancelNotebookCell(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_CancelNotebookCell_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookCellRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.CancelNotebookCell(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_CreateNotebookDownloadFile_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookExportRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.CreateNotebookDownloadFile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_CreateNotebookDownloadFile_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookExportRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.CreateNotebookDownloadFile(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_UploadNotebookAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookFileUploadRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.UploadNotebookAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_UploadNotebookAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookFileUploadRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.UploadNotebookAttachment(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_RemoveNotebookAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookFileUploadRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.RemoveNotebookAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_RemoveNotebookAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq NotebookFileUploadRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.RemoveNotebookAttachment(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_AnnotateTimeline_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AnnotationRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.AnnotateTimeline(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_AnnotateTimeline_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AnnotationRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.AnnotateTimeline(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_GetSecretDefinitions_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := client.GetSecretDefinitions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_GetSecretDefinitions_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := server.GetSecretDefinitions(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_AddSecret_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Secret + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.AddSecret(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_AddSecret_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Secret + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.AddSecret(ctx, &protoReq) + return msg, metadata, err + +} + +func request_API_ModifySecret_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ModifySecretRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.ModifySecret(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_ModifySecret_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ModifySecretRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.ModifySecret(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_API_GetSecret_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_API_GetSecret_0(ctx context.Context, marshaler runtime.Marshaler, client APIClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Secret + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetSecret_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetSecret(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_API_GetSecret_0(ctx context.Context, marshaler runtime.Marshaler, server APIServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Secret + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_API_GetSecret_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetSecret(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterAPIHandlerServer registers the http handlers for service API to "mux". +// UnaryRPC :call APIServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAPIHandlerFromEndpoint instead. +func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server APIServer) error { + + mux.Handle("POST", pattern_API_CreateHunt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CreateHunt", runtime.WithHTTPPathPattern("/api/v1/CreateHunt")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_CreateHunt_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_CreateHunt_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_API_EstimateHunt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/EstimateHunt", runtime.WithHTTPPathPattern("/api/v1/EstimateHunt")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_EstimateHunt_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_EstimateHunt_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetHuntTable_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetHuntTable", runtime.WithHTTPPathPattern("/api/v1/GetHuntTable")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_GetHuntTable_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetHuntTable_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_ListHunts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ListHunts", runtime.WithHTTPPathPattern("/api/v1/ListHunts")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_ListHunts_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_ListHunts_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetHunt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetHunt", runtime.WithHTTPPathPattern("/api/v1/GetHunt")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_GetHunt_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetHunt_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetHuntTags_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetHuntTags", runtime.WithHTTPPathPattern("/api/v1/GetHuntTags")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_GetHuntTags_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetHuntTags_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_API_ModifyHunt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ModifyHunt", runtime.WithHTTPPathPattern("/api/v1/ModifyHunt")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_ModifyHunt_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_ModifyHunt_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetHuntFlows_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetHuntFlows", runtime.WithHTTPPathPattern("/api/v1/GetHuntFlows")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_GetHuntFlows_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetHuntFlows_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetHuntResults_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetHuntResults", runtime.WithHTTPPathPattern("/api/v1/GetHuntResults")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_GetHuntResults_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetHuntResults_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_API_NotifyClients_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/NotifyClients", runtime.WithHTTPPathPattern("/api/v1/NotifyClient")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_NotifyClients_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_NotifyClients_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_API_LabelClients_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/LabelClients", runtime.WithHTTPPathPattern("/api/v1/LabelClients")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_LabelClients_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_LabelClients_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_ListClients_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ListClients", runtime.WithHTTPPathPattern("/api/v1/SearchClients")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_ListClients_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_ListClients_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetClient_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetClient", runtime.WithHTTPPathPattern("/api/v1/GetClient/{client_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_GetClient_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetClient_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetClientMetadata_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetClientMetadata", runtime.WithHTTPPathPattern("/api/v1/GetClientMetadata/{client_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_GetClientMetadata_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetClientMetadata_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_API_SetClientMetadata_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetClientMetadata", runtime.WithHTTPPathPattern("/api/v1/SetClientMetadata")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_SetClientMetadata_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_SetClientMetadata_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetClientFlows_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetClientFlows", runtime.WithHTTPPathPattern("/api/v1/GetClientFlows")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_GetClientFlows_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetClientFlows_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetUserUITraits_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetUserUITraits", runtime.WithHTTPPathPattern("/api/v1/GetUserUITraits")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_GetUserUITraits_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetUserUITraits_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_API_SetGUIOptions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetGUIOptions", runtime.WithHTTPPathPattern("/api/v1/SetGUIOptions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_SetGUIOptions_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_SetGUIOptions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) -} + }) -// RegisterAPIHandlerServer registers the http handlers for service API to "mux". -// UnaryRPC :call APIServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAPIHandlerFromEndpoint instead. -func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server APIServer) error { + mux.Handle("GET", pattern_API_GetUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetUsers", runtime.WithHTTPPathPattern("/api/v1/GetUsers")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_API_GetUsers_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } - mux.Handle("POST", pattern_API_CreateHunt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + forward_API_GetUsers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetGlobalUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CreateHunt", runtime.WithHTTPPathPattern("/api/v1/CreateHunt")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetGlobalUsers", runtime.WithHTTPPathPattern("/api/v1/GetGlobalUsers")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_CreateHunt_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetGlobalUsers_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2009,22 +3020,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_CreateHunt_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetGlobalUsers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_EstimateHunt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetUserRoles_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/EstimateHunt", runtime.WithHTTPPathPattern("/api/v1/EstimateHunt")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetUserRoles", runtime.WithHTTPPathPattern("/api/v1/GetUserRoles")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_EstimateHunt_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetUserRoles_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2032,22 +3043,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_EstimateHunt_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetUserRoles_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_ListHunts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_SetUserRoles_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ListHunts", runtime.WithHTTPPathPattern("/api/v1/ListHunts")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetUserRoles", runtime.WithHTTPPathPattern("/api/v1/SetUserRoles")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_ListHunts_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_SetUserRoles_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2055,22 +3066,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_ListHunts_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_SetUserRoles_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetHunt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetHunt", runtime.WithHTTPPathPattern("/api/v1/GetHunt")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetUser", runtime.WithHTTPPathPattern("/api/v1/GetUser/{name}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetHunt_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetUser_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2078,22 +3089,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetHunt_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetUser_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_ModifyHunt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ModifyHunt", runtime.WithHTTPPathPattern("/api/v1/ModifyHunt")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CreateUser", runtime.WithHTTPPathPattern("/api/v1/CreateUser")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_ModifyHunt_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_CreateUser_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2101,22 +3112,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_ModifyHunt_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_CreateUser_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetHuntFlows_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetUserFavorites_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetHuntFlows", runtime.WithHTTPPathPattern("/api/v1/GetHuntFlows")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetUserFavorites", runtime.WithHTTPPathPattern("/api/v1/GetUserFavorites")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetHuntFlows_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetUserFavorites_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2124,22 +3135,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetHuntFlows_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetUserFavorites_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetHuntResults_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_SetPassword_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetHuntResults", runtime.WithHTTPPathPattern("/api/v1/GetHuntResults")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetPassword", runtime.WithHTTPPathPattern("/api/v1/SetPassword")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetHuntResults_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_SetPassword_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2147,22 +3158,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetHuntResults_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_SetPassword_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_NotifyClients_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_VFSListDirectory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/NotifyClients", runtime.WithHTTPPathPattern("/api/v1/NotifyClient")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/VFSListDirectory", runtime.WithHTTPPathPattern("/api/v1/VFSListDirectory/{client_id}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_NotifyClients_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_VFSListDirectory_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2170,22 +3181,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_NotifyClients_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_VFSListDirectory_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_LabelClients_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_VFSListDirectoryFiles_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/LabelClients", runtime.WithHTTPPathPattern("/api/v1/LabelClients")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/VFSListDirectoryFiles", runtime.WithHTTPPathPattern("/api/v1/VFSListDirectoryFiles")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_LabelClients_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_VFSListDirectoryFiles_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2193,22 +3204,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_LabelClients_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_VFSListDirectoryFiles_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_ListClients_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_VFSRefreshDirectory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ListClients", runtime.WithHTTPPathPattern("/api/v1/SearchClients")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/VFSRefreshDirectory", runtime.WithHTTPPathPattern("/api/v1/VFSRefreshDirectory")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_ListClients_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_VFSRefreshDirectory_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2216,22 +3227,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_ListClients_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_VFSRefreshDirectory_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetClient_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_VFSStatDirectory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetClient", runtime.WithHTTPPathPattern("/api/v1/GetClient/{client_id}")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/VFSStatDirectory", runtime.WithHTTPPathPattern("/api/v1/VFSStatDirectory")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetClient_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_VFSStatDirectory_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2239,22 +3250,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetClient_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_VFSStatDirectory_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetClientMetadata_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_VFSStatDownload_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetClientMetadata", runtime.WithHTTPPathPattern("/api/v1/GetClientMetadata/{client_id}")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/VFSStatDownload", runtime.WithHTTPPathPattern("/api/v1/VFSStatDownload")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetClientMetadata_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_VFSStatDownload_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2262,22 +3273,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetClientMetadata_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_VFSStatDownload_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_SetClientMetadata_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_VFSDownloadFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetClientMetadata", runtime.WithHTTPPathPattern("/api/v1/SetClientMetadata")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/VFSDownloadFile", runtime.WithHTTPPathPattern("/api/v1/VFSDownloadFile")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_SetClientMetadata_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_VFSDownloadFile_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2285,22 +3296,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_SetClientMetadata_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_VFSDownloadFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetClientFlows_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetTable_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetClientFlows", runtime.WithHTTPPathPattern("/api/v1/GetClientFlows/{client_id}")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetTable", runtime.WithHTTPPathPattern("/api/v1/GetTable")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetClientFlows_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetTable_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2308,22 +3319,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetClientFlows_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetTable_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("HEAD", pattern_API_GetClientFlows_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_SearchFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetClientFlows", runtime.WithHTTPPathPattern("/api/v1/GetClientFlows/{client_id}")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SearchFile", runtime.WithHTTPPathPattern("/api/v1/SearchFile")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetClientFlows_1(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_SearchFile_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2331,22 +3342,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetClientFlows_1(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_SearchFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetUserUITraits_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_CollectArtifact_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetUserUITraits", runtime.WithHTTPPathPattern("/api/v1/GetUserUITraits")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CollectArtifact", runtime.WithHTTPPathPattern("/api/v1/CollectArtifact")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetUserUITraits_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_CollectArtifact_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2354,22 +3365,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetUserUITraits_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_CollectArtifact_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_SetGUIOptions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_CancelFlow_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetGUIOptions", runtime.WithHTTPPathPattern("/api/v1/SetGUIOptions")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CancelFlow", runtime.WithHTTPPathPattern("/api/v1/CancelFlow")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_SetGUIOptions_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_CancelFlow_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2377,22 +3388,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_SetGUIOptions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_CancelFlow_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_ResumeFlow_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetUsers", runtime.WithHTTPPathPattern("/api/v1/GetUsers")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ResumeFlow", runtime.WithHTTPPathPattern("/api/v1/ResumeFlow")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetUsers_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_ResumeFlow_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2400,22 +3411,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetUsers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_ResumeFlow_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetUserFavorites_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetFlowDetails_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetUserFavorites", runtime.WithHTTPPathPattern("/api/v1/GetUserFavorites")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetFlowDetails", runtime.WithHTTPPathPattern("/api/v1/GetFlowDetails")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetUserFavorites_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetFlowDetails_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2423,22 +3434,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetUserFavorites_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetFlowDetails_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_VFSListDirectory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetFlowRequests_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/VFSListDirectory", runtime.WithHTTPPathPattern("/api/v1/VFSListDirectory/{client_id}")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetFlowRequests", runtime.WithHTTPPathPattern("/api/v1/GetFlowRequests")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_VFSListDirectory_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetFlowRequests_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2446,22 +3457,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_VFSListDirectory_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetFlowRequests_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_VFSRefreshDirectory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetKeywordCompletions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/VFSRefreshDirectory", runtime.WithHTTPPathPattern("/api/v1/VFSRefreshDirectory")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetKeywordCompletions", runtime.WithHTTPPathPattern("/api/v1/GetKeywordCompletions")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_VFSRefreshDirectory_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetKeywordCompletions_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2469,22 +3480,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_VFSRefreshDirectory_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetKeywordCompletions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_VFSStatDirectory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_ReformatVQL_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/VFSStatDirectory", runtime.WithHTTPPathPattern("/api/v1/VFSStatDirectory")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ReformatVQL", runtime.WithHTTPPathPattern("/api/v1/ReformatVQL")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_VFSStatDirectory_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_ReformatVQL_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2492,22 +3503,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_VFSStatDirectory_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_ReformatVQL_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_VFSStatDownload_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_GetArtifacts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/VFSStatDownload", runtime.WithHTTPPathPattern("/api/v1/VFSStatDownload")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetArtifacts", runtime.WithHTTPPathPattern("/api/v1/GetArtifacts")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_VFSStatDownload_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetArtifacts_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2515,22 +3526,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_VFSStatDownload_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetArtifacts_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetTable_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetArtifactFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetTable", runtime.WithHTTPPathPattern("/api/v1/GetTable")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetArtifactFile", runtime.WithHTTPPathPattern("/api/v1/GetArtifactFile")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetTable_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetArtifactFile_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2538,22 +3549,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetTable_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetArtifactFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_CollectArtifact_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_SetArtifactFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CollectArtifact", runtime.WithHTTPPathPattern("/api/v1/CollectArtifact")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetArtifactFile", runtime.WithHTTPPathPattern("/api/v1/SetArtifactFile")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_CollectArtifact_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_SetArtifactFile_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2561,22 +3572,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_CollectArtifact_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_SetArtifactFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_CancelFlow_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_LoadArtifactPack_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CancelFlow", runtime.WithHTTPPathPattern("/api/v1/CancelFlow")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/LoadArtifactPack", runtime.WithHTTPPathPattern("/api/v1/LoadArtifactPack")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_CancelFlow_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_LoadArtifactPack_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2584,22 +3595,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_CancelFlow_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_LoadArtifactPack_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_ArchiveFlow_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_SearchDocs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ArchiveFlow", runtime.WithHTTPPathPattern("/api/v1/ArchiveFlow")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SearchDocs", runtime.WithHTTPPathPattern("/api/v1/SearchDocs")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_ArchiveFlow_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_SearchDocs_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2607,22 +3618,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_ArchiveFlow_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_SearchDocs_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetFlowDetails_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetToolInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetFlowDetails", runtime.WithHTTPPathPattern("/api/v1/GetFlowDetails")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetToolInfo", runtime.WithHTTPPathPattern("/api/v1/GetToolInfo")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetFlowDetails_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetToolInfo_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2630,22 +3641,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetFlowDetails_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetToolInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetFlowRequests_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_SetToolInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetFlowRequests", runtime.WithHTTPPathPattern("/api/v1/GetFlowRequests")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetToolInfo", runtime.WithHTTPPathPattern("/api/v1/SetToolInfo")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetFlowRequests_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_SetToolInfo_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2653,22 +3664,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetFlowRequests_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_SetToolInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetKeywordCompletions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_GetReport_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetKeywordCompletions", runtime.WithHTTPPathPattern("/api/v1/GetKeywordCompletions")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetReport", runtime.WithHTTPPathPattern("/api/v1/GetReport")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetKeywordCompletions_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetReport_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2676,22 +3687,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetKeywordCompletions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetReport_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_GetArtifacts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetServerMonitoringState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetArtifacts", runtime.WithHTTPPathPattern("/api/v1/GetArtifacts")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetServerMonitoringState", runtime.WithHTTPPathPattern("/api/v1/GetServerMonitoringState")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetArtifacts_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetServerMonitoringState_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2699,22 +3710,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetArtifacts_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetServerMonitoringState_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetArtifactFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_SetServerMonitoringState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetArtifactFile", runtime.WithHTTPPathPattern("/api/v1/GetArtifactFile")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetServerMonitoringState", runtime.WithHTTPPathPattern("/api/v1/SetServerMonitoringState")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetArtifactFile_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_SetServerMonitoringState_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2722,22 +3733,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetArtifactFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_SetServerMonitoringState_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_SetArtifactFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetClientMonitoringState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetArtifactFile", runtime.WithHTTPPathPattern("/api/v1/SetArtifactFile")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetClientMonitoringState", runtime.WithHTTPPathPattern("/api/v1/GetClientMonitoringState")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_SetArtifactFile_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetClientMonitoringState_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2745,22 +3756,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_SetArtifactFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetClientMonitoringState_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_LoadArtifactPack_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_SetClientMonitoringState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/LoadArtifactPack", runtime.WithHTTPPathPattern("/api/v1/LoadArtifactPack")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetClientMonitoringState", runtime.WithHTTPPathPattern("/api/v1/SetClientMonitoringState")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_LoadArtifactPack_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_SetClientMonitoringState_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2768,22 +3779,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_LoadArtifactPack_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_SetClientMonitoringState_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetToolInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_ListAvailableEventResults_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetToolInfo", runtime.WithHTTPPathPattern("/api/v1/GetToolInfo")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ListAvailableEventResults", runtime.WithHTTPPathPattern("/api/v1/ListAvailableEventResults")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetToolInfo_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_ListAvailableEventResults_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2791,22 +3802,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetToolInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_ListAvailableEventResults_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_SetToolInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_CreateDownloadFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetToolInfo", runtime.WithHTTPPathPattern("/api/v1/SetToolInfo")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CreateDownloadFile", runtime.WithHTTPPathPattern("/api/v1/CreateDownload")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_SetToolInfo_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_CreateDownloadFile_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2814,22 +3825,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_SetToolInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_CreateDownloadFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_GetReport_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetNotebooks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetReport", runtime.WithHTTPPathPattern("/api/v1/GetReport")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetNotebooks", runtime.WithHTTPPathPattern("/api/v1/GetNotebooks")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetReport_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetNotebooks_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2837,22 +3848,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetReport_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetNotebooks_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetServerMonitoringState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_NewNotebook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetServerMonitoringState", runtime.WithHTTPPathPattern("/api/v1/GetServerMonitoringState")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/NewNotebook", runtime.WithHTTPPathPattern("/api/v1/NewNotebook")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetServerMonitoringState_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_NewNotebook_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2860,22 +3871,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetServerMonitoringState_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_NewNotebook_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_SetServerMonitoringState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_UpdateNotebook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetServerMonitoringState", runtime.WithHTTPPathPattern("/api/v1/SetServerMonitoringState")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/UpdateNotebook", runtime.WithHTTPPathPattern("/api/v1/UpdateNotebook")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_SetServerMonitoringState_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_UpdateNotebook_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2883,22 +3894,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_SetServerMonitoringState_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_UpdateNotebook_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetClientMonitoringState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_DeleteNotebook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetClientMonitoringState", runtime.WithHTTPPathPattern("/api/v1/GetClientMonitoringState")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/DeleteNotebook", runtime.WithHTTPPathPattern("/api/v1/DeleteNotebook")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetClientMonitoringState_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_DeleteNotebook_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2906,22 +3917,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetClientMonitoringState_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_DeleteNotebook_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_SetClientMonitoringState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_NewNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/SetClientMonitoringState", runtime.WithHTTPPathPattern("/api/v1/SetClientMonitoringState")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/NewNotebookCell", runtime.WithHTTPPathPattern("/api/v1/NewNotebookCell")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_SetClientMonitoringState_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_NewNotebookCell_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2929,22 +3940,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_SetClientMonitoringState_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_NewNotebookCell_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_ListAvailableEventResults_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ListAvailableEventResults", runtime.WithHTTPPathPattern("/api/v1/ListAvailableEventResults")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetNotebookCell", runtime.WithHTTPPathPattern("/api/v1/GetNotebookCell")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_ListAvailableEventResults_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetNotebookCell_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2952,22 +3963,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_ListAvailableEventResults_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetNotebookCell_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_CreateDownloadFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_UpdateNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CreateDownloadFile", runtime.WithHTTPPathPattern("/api/v1/CreateDownload")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/UpdateNotebookCell", runtime.WithHTTPPathPattern("/api/v1/UpdateNotebookCell")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_CreateDownloadFile_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_UpdateNotebookCell_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2975,22 +3986,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_CreateDownloadFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_UpdateNotebookCell_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetNotebooks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_RevertNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetNotebooks", runtime.WithHTTPPathPattern("/api/v1/GetNotebooks")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/RevertNotebookCell", runtime.WithHTTPPathPattern("/api/v1/RevertNotebookCell")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetNotebooks_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_RevertNotebookCell_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -2998,22 +4009,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetNotebooks_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_RevertNotebookCell_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_NewNotebook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_CancelNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/NewNotebook", runtime.WithHTTPPathPattern("/api/v1/NewNotebook")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CancelNotebookCell", runtime.WithHTTPPathPattern("/api/v1/CancelNotebookCell")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_NewNotebook_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_CancelNotebookCell_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -3021,22 +4032,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_NewNotebook_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_CancelNotebookCell_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_UpdateNotebook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_CreateNotebookDownloadFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/UpdateNotebook", runtime.WithHTTPPathPattern("/api/v1/UpdateNotebook")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CreateNotebookDownloadFile", runtime.WithHTTPPathPattern("/api/v1/CreateNotebookDownloadFile")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_UpdateNotebook_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_CreateNotebookDownloadFile_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -3044,22 +4055,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_UpdateNotebook_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_CreateNotebookDownloadFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_NewNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_UploadNotebookAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/NewNotebookCell", runtime.WithHTTPPathPattern("/api/v1/NewNotebookCell")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/UploadNotebookAttachment", runtime.WithHTTPPathPattern("/api/v1/UploadNotebookAttachment")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_NewNotebookCell_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_UploadNotebookAttachment_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -3067,22 +4078,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_NewNotebookCell_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_UploadNotebookAttachment_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_RemoveNotebookAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetNotebookCell", runtime.WithHTTPPathPattern("/api/v1/GetNotebookCell")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/RemoveNotebookAttachment", runtime.WithHTTPPathPattern("/api/v1/RemoveNotebookAttachment")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_GetNotebookCell_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_RemoveNotebookAttachment_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -3090,22 +4101,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_GetNotebookCell_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_RemoveNotebookAttachment_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_UpdateNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_AnnotateTimeline_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/UpdateNotebookCell", runtime.WithHTTPPathPattern("/api/v1/UpdateNotebookCell")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/AnnotateTimeline", runtime.WithHTTPPathPattern("/api/v1/AnnotateTimeline")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_UpdateNotebookCell_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_AnnotateTimeline_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -3113,22 +4124,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_UpdateNotebookCell_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_AnnotateTimeline_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_CancelNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetSecretDefinitions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CancelNotebookCell", runtime.WithHTTPPathPattern("/api/v1/CancelNotebookCell")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetSecretDefinitions", runtime.WithHTTPPathPattern("/api/v1/GetSecretDefinitions")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_CancelNotebookCell_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetSecretDefinitions_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -3136,22 +4147,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_CancelNotebookCell_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetSecretDefinitions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_CreateNotebookDownloadFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_AddSecret_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/CreateNotebookDownloadFile", runtime.WithHTTPPathPattern("/api/v1/CreateNotebookDownloadFile")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/AddSecret", runtime.WithHTTPPathPattern("/api/v1/AddSecret")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_CreateNotebookDownloadFile_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_AddSecret_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -3159,22 +4170,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_CreateNotebookDownloadFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_AddSecret_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_UploadNotebookAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_ModifySecret_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/UploadNotebookAttachment", runtime.WithHTTPPathPattern("/api/v1/UploadNotebookAttachment")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ModifySecret", runtime.WithHTTPPathPattern("/api/v1/ModifySecret")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_UploadNotebookAttachment_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_ModifySecret_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -3182,22 +4193,22 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_UploadNotebookAttachment_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_ModifySecret_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_ExportNotebook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetSecret_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/ExportNotebook", runtime.WithHTTPPathPattern("/api/v1/ExportNotebook")) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/proto.API/GetSecret", runtime.WithHTTPPathPattern("/api/v1/GetSecret")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_API_ExportNotebook_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_API_GetSecret_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -3205,7 +4216,7 @@ func RegisterAPIHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return } - forward_API_ExportNotebook_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetSecret_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) @@ -3290,6 +4301,26 @@ func RegisterAPIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) + mux.Handle("GET", pattern_API_GetHuntTable_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetHuntTable", runtime.WithHTTPPathPattern("/api/v1/GetHuntTable")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_GetHuntTable_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetHuntTable_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_API_ListHunts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -3330,6 +4361,26 @@ func RegisterAPIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) + mux.Handle("GET", pattern_API_GetHuntTags_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetHuntTags", runtime.WithHTTPPathPattern("/api/v1/GetHuntTags")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_GetHuntTags_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetHuntTags_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_API_ModifyHunt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -3474,179 +4525,299 @@ func RegisterAPIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetClientMetadata", runtime.WithHTTPPathPattern("/api/v1/GetClientMetadata/{client_id}")) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetClientMetadata", runtime.WithHTTPPathPattern("/api/v1/GetClientMetadata/{client_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_GetClientMetadata_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetClientMetadata_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_API_SetClientMetadata_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/SetClientMetadata", runtime.WithHTTPPathPattern("/api/v1/SetClientMetadata")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_SetClientMetadata_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_SetClientMetadata_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetClientFlows_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetClientFlows", runtime.WithHTTPPathPattern("/api/v1/GetClientFlows")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_GetClientFlows_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetClientFlows_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetUserUITraits_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetUserUITraits", runtime.WithHTTPPathPattern("/api/v1/GetUserUITraits")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_GetUserUITraits_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetUserUITraits_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_API_SetGUIOptions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/SetGUIOptions", runtime.WithHTTPPathPattern("/api/v1/SetGUIOptions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_SetGUIOptions_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_SetGUIOptions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetUsers", runtime.WithHTTPPathPattern("/api/v1/GetUsers")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_GetUsers_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetUsers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetGlobalUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetGlobalUsers", runtime.WithHTTPPathPattern("/api/v1/GetGlobalUsers")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_API_GetClientMetadata_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_API_GetGlobalUsers_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_API_GetClientMetadata_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetGlobalUsers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_SetClientMetadata_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetUserRoles_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/SetClientMetadata", runtime.WithHTTPPathPattern("/api/v1/SetClientMetadata")) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetUserRoles", runtime.WithHTTPPathPattern("/api/v1/GetUserRoles")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_API_SetClientMetadata_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_API_GetUserRoles_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_API_SetClientMetadata_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetUserRoles_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetClientFlows_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_SetUserRoles_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetClientFlows", runtime.WithHTTPPathPattern("/api/v1/GetClientFlows/{client_id}")) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/SetUserRoles", runtime.WithHTTPPathPattern("/api/v1/SetUserRoles")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_API_GetClientFlows_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_API_SetUserRoles_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_API_GetClientFlows_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_SetUserRoles_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("HEAD", pattern_API_GetClientFlows_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetClientFlows", runtime.WithHTTPPathPattern("/api/v1/GetClientFlows/{client_id}")) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetUser", runtime.WithHTTPPathPattern("/api/v1/GetUser/{name}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_API_GetClientFlows_1(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_API_GetUser_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_API_GetClientFlows_1(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetUser_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetUserUITraits_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetUserUITraits", runtime.WithHTTPPathPattern("/api/v1/GetUserUITraits")) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/CreateUser", runtime.WithHTTPPathPattern("/api/v1/CreateUser")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_API_GetUserUITraits_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_API_CreateUser_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_API_GetUserUITraits_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_CreateUser_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("POST", pattern_API_SetGUIOptions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_GetUserFavorites_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/SetGUIOptions", runtime.WithHTTPPathPattern("/api/v1/SetGUIOptions")) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetUserFavorites", runtime.WithHTTPPathPattern("/api/v1/GetUserFavorites")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_API_SetGUIOptions_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_API_GetUserFavorites_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_API_SetGUIOptions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetUserFavorites_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_SetPassword_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetUsers", runtime.WithHTTPPathPattern("/api/v1/GetUsers")) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/SetPassword", runtime.WithHTTPPathPattern("/api/v1/SetPassword")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_API_GetUsers_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_API_SetPassword_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_API_GetUsers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_SetPassword_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_GetUserFavorites_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_VFSListDirectory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetUserFavorites", runtime.WithHTTPPathPattern("/api/v1/GetUserFavorites")) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/VFSListDirectory", runtime.WithHTTPPathPattern("/api/v1/VFSListDirectory/{client_id}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_API_GetUserFavorites_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_API_VFSListDirectory_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_API_GetUserFavorites_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_VFSListDirectory_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_API_VFSListDirectory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_API_VFSListDirectoryFiles_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/VFSListDirectory", runtime.WithHTTPPathPattern("/api/v1/VFSListDirectory/{client_id}")) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/VFSListDirectoryFiles", runtime.WithHTTPPathPattern("/api/v1/VFSListDirectoryFiles")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_API_VFSListDirectory_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_API_VFSListDirectoryFiles_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_API_VFSListDirectory_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_VFSListDirectoryFiles_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) @@ -3710,6 +4881,26 @@ func RegisterAPIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) + mux.Handle("POST", pattern_API_VFSDownloadFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/VFSDownloadFile", runtime.WithHTTPPathPattern("/api/v1/VFSDownloadFile")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_VFSDownloadFile_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_VFSDownloadFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_API_GetTable_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -3730,6 +4921,26 @@ func RegisterAPIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) + mux.Handle("POST", pattern_API_SearchFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/SearchFile", runtime.WithHTTPPathPattern("/api/v1/SearchFile")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_SearchFile_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_SearchFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_API_CollectArtifact_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -3770,23 +4981,23 @@ func RegisterAPIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) - mux.Handle("POST", pattern_API_ArchiveFlow_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_ResumeFlow_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/ArchiveFlow", runtime.WithHTTPPathPattern("/api/v1/ArchiveFlow")) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/ResumeFlow", runtime.WithHTTPPathPattern("/api/v1/ResumeFlow")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_API_ArchiveFlow_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_API_ResumeFlow_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_API_ArchiveFlow_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_ResumeFlow_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) @@ -3850,6 +5061,26 @@ func RegisterAPIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) + mux.Handle("POST", pattern_API_ReformatVQL_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/ReformatVQL", runtime.WithHTTPPathPattern("/api/v1/ReformatVQL")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_ReformatVQL_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_ReformatVQL_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_API_GetArtifacts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -3930,6 +5161,26 @@ func RegisterAPIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) + mux.Handle("GET", pattern_API_SearchDocs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/SearchDocs", runtime.WithHTTPPathPattern("/api/v1/SearchDocs")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_SearchDocs_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_SearchDocs_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_API_GetToolInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -4170,6 +5421,26 @@ func RegisterAPIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) + mux.Handle("POST", pattern_API_DeleteNotebook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/DeleteNotebook", runtime.WithHTTPPathPattern("/api/v1/DeleteNotebook")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_DeleteNotebook_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_DeleteNotebook_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_API_NewNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -4230,6 +5501,26 @@ func RegisterAPIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) + mux.Handle("POST", pattern_API_RevertNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/RevertNotebookCell", runtime.WithHTTPPathPattern("/api/v1/RevertNotebookCell")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_RevertNotebookCell_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_RevertNotebookCell_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_API_CancelNotebookCell_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -4290,23 +5581,123 @@ func RegisterAPIHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) - mux.Handle("POST", pattern_API_ExportNotebook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_API_RemoveNotebookAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/RemoveNotebookAttachment", runtime.WithHTTPPathPattern("/api/v1/RemoveNotebookAttachment")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_RemoveNotebookAttachment_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_RemoveNotebookAttachment_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_API_AnnotateTimeline_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/AnnotateTimeline", runtime.WithHTTPPathPattern("/api/v1/AnnotateTimeline")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_AnnotateTimeline_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_AnnotateTimeline_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetSecretDefinitions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetSecretDefinitions", runtime.WithHTTPPathPattern("/api/v1/GetSecretDefinitions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_GetSecretDefinitions_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_GetSecretDefinitions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_API_AddSecret_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/AddSecret", runtime.WithHTTPPathPattern("/api/v1/AddSecret")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_AddSecret_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_AddSecret_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_API_ModifySecret_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/ModifySecret", runtime.WithHTTPPathPattern("/api/v1/ModifySecret")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_API_ModifySecret_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_API_ModifySecret_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_API_GetSecret_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/ExportNotebook", runtime.WithHTTPPathPattern("/api/v1/ExportNotebook")) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/proto.API/GetSecret", runtime.WithHTTPPathPattern("/api/v1/GetSecret")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_API_ExportNotebook_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_API_GetSecret_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_API_ExportNotebook_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_API_GetSecret_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) @@ -4318,10 +5709,14 @@ var ( pattern_API_EstimateHunt_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "EstimateHunt"}, "")) + pattern_API_GetHuntTable_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetHuntTable"}, "")) + pattern_API_ListHunts_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "ListHunts"}, "")) pattern_API_GetHunt_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetHunt"}, "")) + pattern_API_GetHuntTags_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetHuntTags"}, "")) + pattern_API_ModifyHunt_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "ModifyHunt"}, "")) pattern_API_GetHuntFlows_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetHuntFlows"}, "")) @@ -4340,9 +5735,7 @@ var ( pattern_API_SetClientMetadata_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "SetClientMetadata"}, "")) - pattern_API_GetClientFlows_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "GetClientFlows", "client_id"}, "")) - - pattern_API_GetClientFlows_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "GetClientFlows", "client_id"}, "")) + pattern_API_GetClientFlows_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetClientFlows"}, "")) pattern_API_GetUserUITraits_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetUserUITraits"}, "")) @@ -4350,23 +5743,41 @@ var ( pattern_API_GetUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetUsers"}, "")) + pattern_API_GetGlobalUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetGlobalUsers"}, "")) + + pattern_API_GetUserRoles_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetUserRoles"}, "")) + + pattern_API_SetUserRoles_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "SetUserRoles"}, "")) + + pattern_API_GetUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "GetUser", "name"}, "")) + + pattern_API_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "CreateUser"}, "")) + pattern_API_GetUserFavorites_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetUserFavorites"}, "")) + pattern_API_SetPassword_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "SetPassword"}, "")) + pattern_API_VFSListDirectory_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "VFSListDirectory", "client_id"}, "")) + pattern_API_VFSListDirectoryFiles_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "VFSListDirectoryFiles"}, "")) + pattern_API_VFSRefreshDirectory_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "VFSRefreshDirectory"}, "")) pattern_API_VFSStatDirectory_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "VFSStatDirectory"}, "")) pattern_API_VFSStatDownload_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "VFSStatDownload"}, "")) + pattern_API_VFSDownloadFile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "VFSDownloadFile"}, "")) + pattern_API_GetTable_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetTable"}, "")) + pattern_API_SearchFile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "SearchFile"}, "")) + pattern_API_CollectArtifact_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "CollectArtifact"}, "")) pattern_API_CancelFlow_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "CancelFlow"}, "")) - pattern_API_ArchiveFlow_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "ArchiveFlow"}, "")) + pattern_API_ResumeFlow_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "ResumeFlow"}, "")) pattern_API_GetFlowDetails_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetFlowDetails"}, "")) @@ -4374,6 +5785,8 @@ var ( pattern_API_GetKeywordCompletions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetKeywordCompletions"}, "")) + pattern_API_ReformatVQL_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "ReformatVQL"}, "")) + pattern_API_GetArtifacts_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetArtifacts"}, "")) pattern_API_GetArtifactFile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetArtifactFile"}, "")) @@ -4382,6 +5795,8 @@ var ( pattern_API_LoadArtifactPack_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "LoadArtifactPack"}, "")) + pattern_API_SearchDocs_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "SearchDocs"}, "")) + pattern_API_GetToolInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetToolInfo"}, "")) pattern_API_SetToolInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "SetToolInfo"}, "")) @@ -4406,19 +5821,33 @@ var ( pattern_API_UpdateNotebook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "UpdateNotebook"}, "")) + pattern_API_DeleteNotebook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "DeleteNotebook"}, "")) + pattern_API_NewNotebookCell_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "NewNotebookCell"}, "")) pattern_API_GetNotebookCell_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetNotebookCell"}, "")) pattern_API_UpdateNotebookCell_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "UpdateNotebookCell"}, "")) + pattern_API_RevertNotebookCell_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "RevertNotebookCell"}, "")) + pattern_API_CancelNotebookCell_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "CancelNotebookCell"}, "")) pattern_API_CreateNotebookDownloadFile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "CreateNotebookDownloadFile"}, "")) pattern_API_UploadNotebookAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "UploadNotebookAttachment"}, "")) - pattern_API_ExportNotebook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "ExportNotebook"}, "")) + pattern_API_RemoveNotebookAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "RemoveNotebookAttachment"}, "")) + + pattern_API_AnnotateTimeline_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "AnnotateTimeline"}, "")) + + pattern_API_GetSecretDefinitions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetSecretDefinitions"}, "")) + + pattern_API_AddSecret_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "AddSecret"}, "")) + + pattern_API_ModifySecret_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "ModifySecret"}, "")) + + pattern_API_GetSecret_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "GetSecret"}, "")) ) var ( @@ -4426,10 +5855,14 @@ var ( forward_API_EstimateHunt_0 = runtime.ForwardResponseMessage + forward_API_GetHuntTable_0 = runtime.ForwardResponseMessage + forward_API_ListHunts_0 = runtime.ForwardResponseMessage forward_API_GetHunt_0 = runtime.ForwardResponseMessage + forward_API_GetHuntTags_0 = runtime.ForwardResponseMessage + forward_API_ModifyHunt_0 = runtime.ForwardResponseMessage forward_API_GetHuntFlows_0 = runtime.ForwardResponseMessage @@ -4450,31 +5883,47 @@ var ( forward_API_GetClientFlows_0 = runtime.ForwardResponseMessage - forward_API_GetClientFlows_1 = runtime.ForwardResponseMessage - forward_API_GetUserUITraits_0 = runtime.ForwardResponseMessage forward_API_SetGUIOptions_0 = runtime.ForwardResponseMessage forward_API_GetUsers_0 = runtime.ForwardResponseMessage + forward_API_GetGlobalUsers_0 = runtime.ForwardResponseMessage + + forward_API_GetUserRoles_0 = runtime.ForwardResponseMessage + + forward_API_SetUserRoles_0 = runtime.ForwardResponseMessage + + forward_API_GetUser_0 = runtime.ForwardResponseMessage + + forward_API_CreateUser_0 = runtime.ForwardResponseMessage + forward_API_GetUserFavorites_0 = runtime.ForwardResponseMessage + forward_API_SetPassword_0 = runtime.ForwardResponseMessage + forward_API_VFSListDirectory_0 = runtime.ForwardResponseMessage + forward_API_VFSListDirectoryFiles_0 = runtime.ForwardResponseMessage + forward_API_VFSRefreshDirectory_0 = runtime.ForwardResponseMessage forward_API_VFSStatDirectory_0 = runtime.ForwardResponseMessage forward_API_VFSStatDownload_0 = runtime.ForwardResponseMessage + forward_API_VFSDownloadFile_0 = runtime.ForwardResponseMessage + forward_API_GetTable_0 = runtime.ForwardResponseMessage + forward_API_SearchFile_0 = runtime.ForwardResponseMessage + forward_API_CollectArtifact_0 = runtime.ForwardResponseMessage forward_API_CancelFlow_0 = runtime.ForwardResponseMessage - forward_API_ArchiveFlow_0 = runtime.ForwardResponseMessage + forward_API_ResumeFlow_0 = runtime.ForwardResponseMessage forward_API_GetFlowDetails_0 = runtime.ForwardResponseMessage @@ -4482,6 +5931,8 @@ var ( forward_API_GetKeywordCompletions_0 = runtime.ForwardResponseMessage + forward_API_ReformatVQL_0 = runtime.ForwardResponseMessage + forward_API_GetArtifacts_0 = runtime.ForwardResponseMessage forward_API_GetArtifactFile_0 = runtime.ForwardResponseMessage @@ -4490,6 +5941,8 @@ var ( forward_API_LoadArtifactPack_0 = runtime.ForwardResponseMessage + forward_API_SearchDocs_0 = runtime.ForwardResponseMessage + forward_API_GetToolInfo_0 = runtime.ForwardResponseMessage forward_API_SetToolInfo_0 = runtime.ForwardResponseMessage @@ -4514,17 +5967,31 @@ var ( forward_API_UpdateNotebook_0 = runtime.ForwardResponseMessage + forward_API_DeleteNotebook_0 = runtime.ForwardResponseMessage + forward_API_NewNotebookCell_0 = runtime.ForwardResponseMessage forward_API_GetNotebookCell_0 = runtime.ForwardResponseMessage forward_API_UpdateNotebookCell_0 = runtime.ForwardResponseMessage + forward_API_RevertNotebookCell_0 = runtime.ForwardResponseMessage + forward_API_CancelNotebookCell_0 = runtime.ForwardResponseMessage forward_API_CreateNotebookDownloadFile_0 = runtime.ForwardResponseMessage forward_API_UploadNotebookAttachment_0 = runtime.ForwardResponseMessage - forward_API_ExportNotebook_0 = runtime.ForwardResponseMessage + forward_API_RemoveNotebookAttachment_0 = runtime.ForwardResponseMessage + + forward_API_AnnotateTimeline_0 = runtime.ForwardResponseMessage + + forward_API_GetSecretDefinitions_0 = runtime.ForwardResponseMessage + + forward_API_AddSecret_0 = runtime.ForwardResponseMessage + + forward_API_ModifySecret_0 = runtime.ForwardResponseMessage + + forward_API_GetSecret_0 = runtime.ForwardResponseMessage ) diff --git a/api/proto/api.proto b/api/proto/api.proto index 043bb1184..b1c6c44a8 100644 --- a/api/proto/api.proto +++ b/api/proto/api.proto @@ -12,6 +12,7 @@ import "google/protobuf/empty.proto"; import "artifacts.proto"; import "clients.proto"; import "datastore.proto"; +import "docs.proto"; import "health.proto"; import "hunts.proto"; import "flows.proto"; @@ -21,6 +22,9 @@ import "csv.proto"; import "download.proto"; import "completions.proto"; import "vfs_api.proto"; +import "scheduler.proto"; +import "secrets.proto"; +import "timeline_api.proto"; package proto; @@ -45,9 +49,6 @@ message VFSRefreshDirectoryRequest { label: HIDDEN, }]; - // Deprecated - string XXXXvfs_path = 2; - repeated string vfs_components = 4; uint64 depth = 3 [(sem_type) = { @@ -61,6 +62,8 @@ message VFSFileBuffer { uint32 length = 4; bytes data = 5; repeated string components = 6; + string org_id = 7; + optional bool padding = 8; } @@ -75,6 +78,10 @@ message EventRequest { // The node who is requesting the event stream. string node = 2; + + string watcher_name = 3; + + string org_id = 4; } message EventResponse { @@ -92,6 +99,19 @@ message PushEventRequest { string flow_id = 3; bytes jsonl = 4; + + int64 rows = 5; + + string org_id = 6; + + // If false we do not write but just broadcast to all + // listeners. But if true we also write to the local filestore. + bool write = 7; + + // The username source for the event. This can only be set by the + // minion as a trusted impersonation. Other callers will have this + // set to their real username. + string username = 8; } @@ -106,13 +126,20 @@ service API { // Returns an estimate of the number of clients that might be // affected by a hunt. - rpc EstimateHunt(Hunt) returns (HuntStats) { + rpc EstimateHunt(HuntEstimateRequest) returns (HuntStats) { option (google.api.http) = { post: "/api/v1/EstimateHunt", body: "*" }; } + rpc GetHuntTable(GetTableRequest) returns (GetTableResponse) { + option (google.api.http) = { + get: "/api/v1/GetHuntTable", + }; + } + + // Deprecated - hunts are now listed with GetHuntTable() rpc ListHunts(ListHuntsRequest) returns (ListHuntsResponse) { option (google.api.http) = { get: "/api/v1/ListHunts", @@ -125,7 +152,13 @@ service API { }; } - rpc ModifyHunt(Hunt) returns (google.protobuf.Empty) { + rpc GetHuntTags(google.protobuf.Empty) returns (HuntTags) { + option (google.api.http) = { + get: "/api/v1/GetHuntTags", + }; + } + + rpc ModifyHunt(HuntMutation) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/api/v1/ModifyHunt", body: "*" @@ -177,33 +210,27 @@ service API { }; } - rpc SetClientMetadata(ClientMetadata) returns (google.protobuf.Empty) { + rpc SetClientMetadata(SetClientMetadataRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/api/v1/SetClientMetadata", body: "*" }; } - rpc GetClientFlows(ApiFlowRequest) returns (ApiFlowResponse) { + rpc GetClientFlows(GetTableRequest) returns (GetTableResponse) { option (google.api.http) = { - get: "/api/v1/GetClientFlows/{client_id}", - additional_bindings: { - custom: { - kind: "HEAD", - path: "/api/v1/GetClientFlows/{client_id}", - }, - } + get: "/api/v1/GetClientFlows", }; } // Users - rpc GetUserUITraits(google.protobuf.Empty) returns (ApiGrrUser) { + rpc GetUserUITraits(google.protobuf.Empty) returns (ApiUser) { option (google.api.http) = { get: "/api/v1/GetUserUITraits", }; } - rpc SetGUIOptions(SetGUIOptionsRequest) returns (google.protobuf.Empty) { + rpc SetGUIOptions(SetGUIOptionsRequest) returns (SetGUIOptionsResponse) { option (google.api.http) = { post: "/api/v1/SetGUIOptions", body: "*" @@ -217,12 +244,52 @@ service API { }; } + // List all the GUI users in orgs in which we are a member + rpc GetGlobalUsers(google.protobuf.Empty) returns(Users) { + option (google.api.http) = { + get: "/api/v1/GetGlobalUsers", + }; + } + + rpc GetUserRoles(UserRequest) returns(UserRoles) { + option (google.api.http) = { + get: "/api/v1/GetUserRoles", + }; + } + + rpc SetUserRoles(UserRoles) returns(google.protobuf.Empty) { + option (google.api.http) = { + post: "/api/v1/SetUserRoles", + body: "*", + }; + } + + rpc GetUser(UserRequest) returns(VelociraptorUser) { + option(google.api.http) = { + get: "/api/v1/GetUser/{name}", + }; + } + + rpc CreateUser(UpdateUserRequest) returns(google.protobuf.Empty) { + option (google.api.http) = { + post: "/api/v1/CreateUser", + body: "*" + }; + } + rpc GetUserFavorites(Favorite) returns(Favorites) { option (google.api.http) = { get: "/api/v1/GetUserFavorites", }; } + rpc SetPassword(SetPasswordRequest) returns(google.protobuf.Empty) { + option (google.api.http) = { + post: "/api/v1/SetPassword", + body: "*" + }; + } + // VFS rpc VFSListDirectory(VFSListRequest) returns (VFSListResponse) { option (google.api.http) = { @@ -230,6 +297,13 @@ service API { }; } + rpc VFSListDirectoryFiles(GetTableRequest) returns (GetTableResponse) { + option (google.api.http) = { + get: "/api/v1/VFSListDirectoryFiles", + }; + } + + rpc VFSRefreshDirectory(VFSRefreshDirectoryRequest) returns (ArtifactCollectorResponse) { option (google.api.http) = { post: "/api/v1/VFSRefreshDirectory", @@ -249,12 +323,27 @@ service API { }; } + rpc VFSDownloadFile(VFSStatDownloadRequest) returns (StartFlowResponse) { + option (google.api.http) = { + post: "/api/v1/VFSDownloadFile", + body: "*", + }; + } + rpc GetTable(GetTableRequest) returns (GetTableResponse) { option (google.api.http) = { get: "/api/v1/GetTable", }; } + // Facilitate the HexEditor search API + rpc SearchFile(SearchFileRequest) returns (SearchFileResponse) { + option (google.api.http) = { + post: "/api/v1/SearchFile", + body: "*", + }; + } + // Flows rpc CollectArtifact(ArtifactCollectorArgs) returns (ArtifactCollectorResponse) { option (google.api.http) = { @@ -270,14 +359,13 @@ service API { }; } - rpc ArchiveFlow(ApiFlowRequest) returns (StartFlowResponse) { + rpc ResumeFlow(ApiFlowRequest) returns (google.protobuf.Empty) { option (google.api.http) = { - post: "/api/v1/ArchiveFlow", + post: "/api/v1/ResumeFlow", body: "*" }; } - rpc GetFlowDetails(ApiFlowRequest) returns (FlowDetails) { option (google.api.http) = { get: "/api/v1/GetFlowDetails", @@ -290,12 +378,20 @@ service API { }; } + // VQL assistance rpc GetKeywordCompletions(google.protobuf.Empty) returns (KeywordCompletions) { option (google.api.http) = { get: "/api/v1/GetKeywordCompletions", }; } + rpc ReformatVQL(ReformatVQLMessage) returns (ReformatVQLMessage) { + option (google.api.http) = { + post: "/api/v1/ReformatVQL", + body: "*" + }; + } + // Artifacts rpc GetArtifacts(GetArtifactsRequest) returns (ArtifactDescriptors) { option (google.api.http) = { @@ -310,20 +406,27 @@ service API { }; } - rpc SetArtifactFile(SetArtifactRequest) returns (APIResponse) { + rpc SetArtifactFile(SetArtifactRequest) returns (SetArtifactResponse) { option (google.api.http) = { post: "/api/v1/SetArtifactFile", body: "*", }; } - rpc LoadArtifactPack(VFSFileBuffer) returns (LoadArtifactPackResponse) { + rpc LoadArtifactPack(LoadArtifactPackRequest) returns (LoadArtifactPackResponse) { option (google.api.http) = { post: "/api/v1/LoadArtifactPack", body: "*", }; } + // Documentation + rpc SearchDocs(DocSearchRequest) returns (DocSearchResponses) { + option (google.api.http) = { + get: "/api/v1/SearchDocs", + }; + } + // Tools rpc GetToolInfo(Tool) returns (Tool) { option (google.api.http) = { @@ -417,6 +520,13 @@ service API { }; } + rpc DeleteNotebook(NotebookMetadata) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/api/v1/DeleteNotebook", + body: "*", + }; + } + rpc NewNotebookCell(NotebookCellRequest) returns (NotebookMetadata) { option (google.api.http) = { post: "/api/v1/NewNotebookCell", @@ -437,6 +547,13 @@ service API { }; } + rpc RevertNotebookCell(NotebookCellRequest) returns (NotebookCell) { + option (google.api.http) = { + post: "/api/v1/RevertNotebookCell", + body: "*", + }; + } + rpc CancelNotebookCell(NotebookCellRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/api/v1/CancelNotebookCell", @@ -458,13 +575,49 @@ service API { }; } - rpc ExportNotebook(NotebookExportRequest) returns (google.protobuf.Empty) { + // Remove a notebook attachment. + rpc RemoveNotebookAttachment(NotebookFileUploadRequest) returns (google.protobuf.Empty) { option (google.api.http) = { - post: "/api/v1/ExportNotebook", + post: "/api/v1/RemoveNotebookAttachment", body: "*", }; } + rpc AnnotateTimeline(AnnotationRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/api/v1/AnnotateTimeline", + body: "*", + }; + } + + // Secret management + rpc GetSecretDefinitions(google.protobuf.Empty) returns (SecretDefinitionList) { + option (google.api.http) = { + get: "/api/v1/GetSecretDefinitions", + }; + } + + rpc AddSecret(Secret) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/api/v1/AddSecret", + body: "*", + }; + } + + rpc ModifySecret(ModifySecretRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/api/v1/ModifySecret", + body: "*", + }; + } + + // Returns a redacted version of the secret. + rpc GetSecret(Secret) returns (Secret) { + option (google.api.http) = { + get: "/api/v1/GetSecret", + }; + } + // The below are API client methods - not available over HTTP // This can be used by API clients to fetch file content. @@ -485,6 +638,9 @@ service API { // Push monitoring event to the server. rpc WriteEvent(VQLResponse) returns (google.protobuf.Empty) {} + // Scheduler endpoint for minion scheduling + rpc Scheduler(stream ScheduleRequest) returns (stream ScheduleResponse) {} + // Remote data store access. rpc GetSubject(DataRequest) returns (DataResponse) {} rpc SetSubject(DataRequest) returns (DataResponse) {} @@ -493,4 +649,4 @@ service API { // Health check protocol as in https://github.com/grpc/grpc/blob/master/doc/health-checking.md rpc Check(HealthCheckRequest) returns (HealthCheckResponse); -} \ No newline at end of file +} diff --git a/api/proto/api_grpc.pb.go b/api/proto/api_grpc.pb.go index dd16746ba..4240ead33 100644 --- a/api/proto/api_grpc.pb.go +++ b/api/proto/api_grpc.pb.go @@ -4,10 +4,10 @@ package proto import ( context "context" - empty "github.com/golang/protobuf/ptypes/empty" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" proto2 "www.velocidex.com/golang/velociraptor/actions/proto" proto1 "www.velocidex.com/golang/velociraptor/artifacts/proto" proto "www.velocidex.com/golang/velociraptor/flows/proto" @@ -26,44 +26,62 @@ type APIClient interface { CreateHunt(ctx context.Context, in *Hunt, opts ...grpc.CallOption) (*StartFlowResponse, error) // Returns an estimate of the number of clients that might be // affected by a hunt. - EstimateHunt(ctx context.Context, in *Hunt, opts ...grpc.CallOption) (*HuntStats, error) + EstimateHunt(ctx context.Context, in *HuntEstimateRequest, opts ...grpc.CallOption) (*HuntStats, error) + GetHuntTable(ctx context.Context, in *GetTableRequest, opts ...grpc.CallOption) (*GetTableResponse, error) + // Deprecated - hunts are now listed with GetHuntTable() ListHunts(ctx context.Context, in *ListHuntsRequest, opts ...grpc.CallOption) (*ListHuntsResponse, error) GetHunt(ctx context.Context, in *GetHuntRequest, opts ...grpc.CallOption) (*Hunt, error) - ModifyHunt(ctx context.Context, in *Hunt, opts ...grpc.CallOption) (*empty.Empty, error) + GetHuntTags(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HuntTags, error) + ModifyHunt(ctx context.Context, in *HuntMutation, opts ...grpc.CallOption) (*emptypb.Empty, error) GetHuntFlows(ctx context.Context, in *GetTableRequest, opts ...grpc.CallOption) (*GetTableResponse, error) GetHuntResults(ctx context.Context, in *GetHuntResultsRequest, opts ...grpc.CallOption) (*GetTableResponse, error) // Clients. - NotifyClients(ctx context.Context, in *NotificationRequest, opts ...grpc.CallOption) (*empty.Empty, error) + NotifyClients(ctx context.Context, in *NotificationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) LabelClients(ctx context.Context, in *LabelClientsRequest, opts ...grpc.CallOption) (*APIResponse, error) ListClients(ctx context.Context, in *SearchClientsRequest, opts ...grpc.CallOption) (*SearchClientsResponse, error) GetClient(ctx context.Context, in *GetClientRequest, opts ...grpc.CallOption) (*ApiClient, error) GetClientMetadata(ctx context.Context, in *GetClientRequest, opts ...grpc.CallOption) (*ClientMetadata, error) - SetClientMetadata(ctx context.Context, in *ClientMetadata, opts ...grpc.CallOption) (*empty.Empty, error) - GetClientFlows(ctx context.Context, in *ApiFlowRequest, opts ...grpc.CallOption) (*ApiFlowResponse, error) + SetClientMetadata(ctx context.Context, in *SetClientMetadataRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetClientFlows(ctx context.Context, in *GetTableRequest, opts ...grpc.CallOption) (*GetTableResponse, error) // Users - GetUserUITraits(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*ApiGrrUser, error) - SetGUIOptions(ctx context.Context, in *SetGUIOptionsRequest, opts ...grpc.CallOption) (*empty.Empty, error) + GetUserUITraits(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ApiUser, error) + SetGUIOptions(ctx context.Context, in *SetGUIOptionsRequest, opts ...grpc.CallOption) (*SetGUIOptionsResponse, error) // List all the GUI users known on this server. - GetUsers(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*Users, error) + GetUsers(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Users, error) + // List all the GUI users in orgs in which we are a member + GetGlobalUsers(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Users, error) + GetUserRoles(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserRoles, error) + SetUserRoles(ctx context.Context, in *UserRoles, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetUser(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*VelociraptorUser, error) + CreateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) GetUserFavorites(ctx context.Context, in *Favorite, opts ...grpc.CallOption) (*Favorites, error) + SetPassword(ctx context.Context, in *SetPasswordRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // VFS VFSListDirectory(ctx context.Context, in *VFSListRequest, opts ...grpc.CallOption) (*VFSListResponse, error) + VFSListDirectoryFiles(ctx context.Context, in *GetTableRequest, opts ...grpc.CallOption) (*GetTableResponse, error) VFSRefreshDirectory(ctx context.Context, in *VFSRefreshDirectoryRequest, opts ...grpc.CallOption) (*proto.ArtifactCollectorResponse, error) VFSStatDirectory(ctx context.Context, in *VFSListRequest, opts ...grpc.CallOption) (*VFSListResponse, error) VFSStatDownload(ctx context.Context, in *VFSStatDownloadRequest, opts ...grpc.CallOption) (*proto.VFSDownloadInfo, error) + VFSDownloadFile(ctx context.Context, in *VFSStatDownloadRequest, opts ...grpc.CallOption) (*StartFlowResponse, error) GetTable(ctx context.Context, in *GetTableRequest, opts ...grpc.CallOption) (*GetTableResponse, error) + // Facilitate the HexEditor search API + SearchFile(ctx context.Context, in *SearchFileRequest, opts ...grpc.CallOption) (*SearchFileResponse, error) // Flows CollectArtifact(ctx context.Context, in *proto.ArtifactCollectorArgs, opts ...grpc.CallOption) (*proto.ArtifactCollectorResponse, error) CancelFlow(ctx context.Context, in *ApiFlowRequest, opts ...grpc.CallOption) (*StartFlowResponse, error) - ArchiveFlow(ctx context.Context, in *ApiFlowRequest, opts ...grpc.CallOption) (*StartFlowResponse, error) + ResumeFlow(ctx context.Context, in *ApiFlowRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) GetFlowDetails(ctx context.Context, in *ApiFlowRequest, opts ...grpc.CallOption) (*FlowDetails, error) GetFlowRequests(ctx context.Context, in *ApiFlowRequest, opts ...grpc.CallOption) (*ApiFlowRequestDetails, error) - GetKeywordCompletions(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*KeywordCompletions, error) + // VQL assistance + GetKeywordCompletions(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*KeywordCompletions, error) + ReformatVQL(ctx context.Context, in *ReformatVQLMessage, opts ...grpc.CallOption) (*ReformatVQLMessage, error) // Artifacts GetArtifacts(ctx context.Context, in *GetArtifactsRequest, opts ...grpc.CallOption) (*proto1.ArtifactDescriptors, error) GetArtifactFile(ctx context.Context, in *GetArtifactRequest, opts ...grpc.CallOption) (*GetArtifactResponse, error) - SetArtifactFile(ctx context.Context, in *SetArtifactRequest, opts ...grpc.CallOption) (*APIResponse, error) - LoadArtifactPack(ctx context.Context, in *VFSFileBuffer, opts ...grpc.CallOption) (*LoadArtifactPackResponse, error) + SetArtifactFile(ctx context.Context, in *SetArtifactRequest, opts ...grpc.CallOption) (*SetArtifactResponse, error) + LoadArtifactPack(ctx context.Context, in *LoadArtifactPackRequest, opts ...grpc.CallOption) (*LoadArtifactPackResponse, error) + // Documentation + SearchDocs(ctx context.Context, in *DocSearchRequest, opts ...grpc.CallOption) (*DocSearchResponses, error) // Tools GetToolInfo(ctx context.Context, in *proto1.Tool, opts ...grpc.CallOption) (*proto1.Tool, error) SetToolInfo(ctx context.Context, in *proto1.Tool, opts ...grpc.CallOption) (*proto1.Tool, error) @@ -71,7 +89,7 @@ type APIClient interface { GetReport(ctx context.Context, in *GetReportRequest, opts ...grpc.CallOption) (*GetReportResponse, error) // Server Monitoring Artifacts - manage the Server Monitoring // Service.. - GetServerMonitoringState(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*proto.ArtifactCollectorArgs, error) + GetServerMonitoringState(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*proto.ArtifactCollectorArgs, error) // Server Monitoring Artifacts - manage the Server Monitoring // Service. SetServerMonitoringState(ctx context.Context, in *proto.ArtifactCollectorArgs, opts ...grpc.CallOption) (*proto.ArtifactCollectorArgs, error) @@ -80,7 +98,7 @@ type APIClient interface { GetClientMonitoringState(ctx context.Context, in *proto.GetClientMonitoringStateRequest, opts ...grpc.CallOption) (*proto.ClientEventTable, error) // Client Monitoring Artifacts - manage the Client Monitoring // Service. - SetClientMonitoringState(ctx context.Context, in *proto.ClientEventTable, opts ...grpc.CallOption) (*empty.Empty, error) + SetClientMonitoringState(ctx context.Context, in *proto.ClientEventTable, opts ...grpc.CallOption) (*emptypb.Empty, error) ListAvailableEventResults(ctx context.Context, in *ListAvailableEventResultsRequest, opts ...grpc.CallOption) (*ListAvailableEventResultsResponse, error) // Schedule downloads. CreateDownloadFile(ctx context.Context, in *CreateDownloadRequest, opts ...grpc.CallOption) (*CreateDownloadResponse, error) @@ -88,13 +106,23 @@ type APIClient interface { GetNotebooks(ctx context.Context, in *NotebookCellRequest, opts ...grpc.CallOption) (*Notebooks, error) NewNotebook(ctx context.Context, in *NotebookMetadata, opts ...grpc.CallOption) (*NotebookMetadata, error) UpdateNotebook(ctx context.Context, in *NotebookMetadata, opts ...grpc.CallOption) (*NotebookMetadata, error) + DeleteNotebook(ctx context.Context, in *NotebookMetadata, opts ...grpc.CallOption) (*emptypb.Empty, error) NewNotebookCell(ctx context.Context, in *NotebookCellRequest, opts ...grpc.CallOption) (*NotebookMetadata, error) GetNotebookCell(ctx context.Context, in *NotebookCellRequest, opts ...grpc.CallOption) (*NotebookCell, error) UpdateNotebookCell(ctx context.Context, in *NotebookCellRequest, opts ...grpc.CallOption) (*NotebookCell, error) - CancelNotebookCell(ctx context.Context, in *NotebookCellRequest, opts ...grpc.CallOption) (*empty.Empty, error) - CreateNotebookDownloadFile(ctx context.Context, in *NotebookExportRequest, opts ...grpc.CallOption) (*empty.Empty, error) + RevertNotebookCell(ctx context.Context, in *NotebookCellRequest, opts ...grpc.CallOption) (*NotebookCell, error) + CancelNotebookCell(ctx context.Context, in *NotebookCellRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + CreateNotebookDownloadFile(ctx context.Context, in *NotebookExportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) UploadNotebookAttachment(ctx context.Context, in *NotebookFileUploadRequest, opts ...grpc.CallOption) (*NotebookFileUploadResponse, error) - ExportNotebook(ctx context.Context, in *NotebookExportRequest, opts ...grpc.CallOption) (*empty.Empty, error) + // Remove a notebook attachment. + RemoveNotebookAttachment(ctx context.Context, in *NotebookFileUploadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + AnnotateTimeline(ctx context.Context, in *AnnotationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // Secret management + GetSecretDefinitions(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SecretDefinitionList, error) + AddSecret(ctx context.Context, in *Secret, opts ...grpc.CallOption) (*emptypb.Empty, error) + ModifySecret(ctx context.Context, in *ModifySecretRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // Returns a redacted version of the secret. + GetSecret(ctx context.Context, in *Secret, opts ...grpc.CallOption) (*Secret, error) // This can be used by API clients to fetch file content. VFSGetBuffer(ctx context.Context, in *VFSFileBuffer, opts ...grpc.CallOption) (*VFSFileBuffer, error) // Streaming free form VQL. @@ -102,13 +130,15 @@ type APIClient interface { // Watch for events from the master. WatchEvent(ctx context.Context, in *EventRequest, opts ...grpc.CallOption) (API_WatchEventClient, error) // Push the events to the master - PushEvents(ctx context.Context, in *PushEventRequest, opts ...grpc.CallOption) (*empty.Empty, error) + PushEvents(ctx context.Context, in *PushEventRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // Push monitoring event to the server. - WriteEvent(ctx context.Context, in *proto2.VQLResponse, opts ...grpc.CallOption) (*empty.Empty, error) + WriteEvent(ctx context.Context, in *proto2.VQLResponse, opts ...grpc.CallOption) (*emptypb.Empty, error) + // Scheduler endpoint for minion scheduling + Scheduler(ctx context.Context, opts ...grpc.CallOption) (API_SchedulerClient, error) // Remote data store access. GetSubject(ctx context.Context, in *DataRequest, opts ...grpc.CallOption) (*DataResponse, error) SetSubject(ctx context.Context, in *DataRequest, opts ...grpc.CallOption) (*DataResponse, error) - DeleteSubject(ctx context.Context, in *DataRequest, opts ...grpc.CallOption) (*empty.Empty, error) + DeleteSubject(ctx context.Context, in *DataRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) ListChildren(ctx context.Context, in *DataRequest, opts ...grpc.CallOption) (*ListChildrenResponse, error) // Health check protocol as in https://github.com/grpc/grpc/blob/master/doc/health-checking.md Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) @@ -131,7 +161,7 @@ func (c *aPIClient) CreateHunt(ctx context.Context, in *Hunt, opts ...grpc.CallO return out, nil } -func (c *aPIClient) EstimateHunt(ctx context.Context, in *Hunt, opts ...grpc.CallOption) (*HuntStats, error) { +func (c *aPIClient) EstimateHunt(ctx context.Context, in *HuntEstimateRequest, opts ...grpc.CallOption) (*HuntStats, error) { out := new(HuntStats) err := c.cc.Invoke(ctx, "/proto.API/EstimateHunt", in, out, opts...) if err != nil { @@ -140,6 +170,15 @@ func (c *aPIClient) EstimateHunt(ctx context.Context, in *Hunt, opts ...grpc.Cal return out, nil } +func (c *aPIClient) GetHuntTable(ctx context.Context, in *GetTableRequest, opts ...grpc.CallOption) (*GetTableResponse, error) { + out := new(GetTableResponse) + err := c.cc.Invoke(ctx, "/proto.API/GetHuntTable", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *aPIClient) ListHunts(ctx context.Context, in *ListHuntsRequest, opts ...grpc.CallOption) (*ListHuntsResponse, error) { out := new(ListHuntsResponse) err := c.cc.Invoke(ctx, "/proto.API/ListHunts", in, out, opts...) @@ -158,8 +197,17 @@ func (c *aPIClient) GetHunt(ctx context.Context, in *GetHuntRequest, opts ...grp return out, nil } -func (c *aPIClient) ModifyHunt(ctx context.Context, in *Hunt, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) +func (c *aPIClient) GetHuntTags(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HuntTags, error) { + out := new(HuntTags) + err := c.cc.Invoke(ctx, "/proto.API/GetHuntTags", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) ModifyHunt(ctx context.Context, in *HuntMutation, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/proto.API/ModifyHunt", in, out, opts...) if err != nil { return nil, err @@ -185,8 +233,8 @@ func (c *aPIClient) GetHuntResults(ctx context.Context, in *GetHuntResultsReques return out, nil } -func (c *aPIClient) NotifyClients(ctx context.Context, in *NotificationRequest, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) +func (c *aPIClient) NotifyClients(ctx context.Context, in *NotificationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/proto.API/NotifyClients", in, out, opts...) if err != nil { return nil, err @@ -230,8 +278,8 @@ func (c *aPIClient) GetClientMetadata(ctx context.Context, in *GetClientRequest, return out, nil } -func (c *aPIClient) SetClientMetadata(ctx context.Context, in *ClientMetadata, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) +func (c *aPIClient) SetClientMetadata(ctx context.Context, in *SetClientMetadataRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/proto.API/SetClientMetadata", in, out, opts...) if err != nil { return nil, err @@ -239,8 +287,8 @@ func (c *aPIClient) SetClientMetadata(ctx context.Context, in *ClientMetadata, o return out, nil } -func (c *aPIClient) GetClientFlows(ctx context.Context, in *ApiFlowRequest, opts ...grpc.CallOption) (*ApiFlowResponse, error) { - out := new(ApiFlowResponse) +func (c *aPIClient) GetClientFlows(ctx context.Context, in *GetTableRequest, opts ...grpc.CallOption) (*GetTableResponse, error) { + out := new(GetTableResponse) err := c.cc.Invoke(ctx, "/proto.API/GetClientFlows", in, out, opts...) if err != nil { return nil, err @@ -248,8 +296,8 @@ func (c *aPIClient) GetClientFlows(ctx context.Context, in *ApiFlowRequest, opts return out, nil } -func (c *aPIClient) GetUserUITraits(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*ApiGrrUser, error) { - out := new(ApiGrrUser) +func (c *aPIClient) GetUserUITraits(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ApiUser, error) { + out := new(ApiUser) err := c.cc.Invoke(ctx, "/proto.API/GetUserUITraits", in, out, opts...) if err != nil { return nil, err @@ -257,8 +305,8 @@ func (c *aPIClient) GetUserUITraits(ctx context.Context, in *empty.Empty, opts . return out, nil } -func (c *aPIClient) SetGUIOptions(ctx context.Context, in *SetGUIOptionsRequest, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) +func (c *aPIClient) SetGUIOptions(ctx context.Context, in *SetGUIOptionsRequest, opts ...grpc.CallOption) (*SetGUIOptionsResponse, error) { + out := new(SetGUIOptionsResponse) err := c.cc.Invoke(ctx, "/proto.API/SetGUIOptions", in, out, opts...) if err != nil { return nil, err @@ -266,7 +314,7 @@ func (c *aPIClient) SetGUIOptions(ctx context.Context, in *SetGUIOptionsRequest, return out, nil } -func (c *aPIClient) GetUsers(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*Users, error) { +func (c *aPIClient) GetUsers(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Users, error) { out := new(Users) err := c.cc.Invoke(ctx, "/proto.API/GetUsers", in, out, opts...) if err != nil { @@ -275,6 +323,51 @@ func (c *aPIClient) GetUsers(ctx context.Context, in *empty.Empty, opts ...grpc. return out, nil } +func (c *aPIClient) GetGlobalUsers(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Users, error) { + out := new(Users) + err := c.cc.Invoke(ctx, "/proto.API/GetGlobalUsers", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) GetUserRoles(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserRoles, error) { + out := new(UserRoles) + err := c.cc.Invoke(ctx, "/proto.API/GetUserRoles", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) SetUserRoles(ctx context.Context, in *UserRoles, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/proto.API/SetUserRoles", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) GetUser(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*VelociraptorUser, error) { + out := new(VelociraptorUser) + err := c.cc.Invoke(ctx, "/proto.API/GetUser", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) CreateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/proto.API/CreateUser", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *aPIClient) GetUserFavorites(ctx context.Context, in *Favorite, opts ...grpc.CallOption) (*Favorites, error) { out := new(Favorites) err := c.cc.Invoke(ctx, "/proto.API/GetUserFavorites", in, out, opts...) @@ -284,6 +377,15 @@ func (c *aPIClient) GetUserFavorites(ctx context.Context, in *Favorite, opts ... return out, nil } +func (c *aPIClient) SetPassword(ctx context.Context, in *SetPasswordRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/proto.API/SetPassword", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *aPIClient) VFSListDirectory(ctx context.Context, in *VFSListRequest, opts ...grpc.CallOption) (*VFSListResponse, error) { out := new(VFSListResponse) err := c.cc.Invoke(ctx, "/proto.API/VFSListDirectory", in, out, opts...) @@ -293,6 +395,15 @@ func (c *aPIClient) VFSListDirectory(ctx context.Context, in *VFSListRequest, op return out, nil } +func (c *aPIClient) VFSListDirectoryFiles(ctx context.Context, in *GetTableRequest, opts ...grpc.CallOption) (*GetTableResponse, error) { + out := new(GetTableResponse) + err := c.cc.Invoke(ctx, "/proto.API/VFSListDirectoryFiles", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *aPIClient) VFSRefreshDirectory(ctx context.Context, in *VFSRefreshDirectoryRequest, opts ...grpc.CallOption) (*proto.ArtifactCollectorResponse, error) { out := new(proto.ArtifactCollectorResponse) err := c.cc.Invoke(ctx, "/proto.API/VFSRefreshDirectory", in, out, opts...) @@ -320,6 +431,15 @@ func (c *aPIClient) VFSStatDownload(ctx context.Context, in *VFSStatDownloadRequ return out, nil } +func (c *aPIClient) VFSDownloadFile(ctx context.Context, in *VFSStatDownloadRequest, opts ...grpc.CallOption) (*StartFlowResponse, error) { + out := new(StartFlowResponse) + err := c.cc.Invoke(ctx, "/proto.API/VFSDownloadFile", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *aPIClient) GetTable(ctx context.Context, in *GetTableRequest, opts ...grpc.CallOption) (*GetTableResponse, error) { out := new(GetTableResponse) err := c.cc.Invoke(ctx, "/proto.API/GetTable", in, out, opts...) @@ -329,6 +449,15 @@ func (c *aPIClient) GetTable(ctx context.Context, in *GetTableRequest, opts ...g return out, nil } +func (c *aPIClient) SearchFile(ctx context.Context, in *SearchFileRequest, opts ...grpc.CallOption) (*SearchFileResponse, error) { + out := new(SearchFileResponse) + err := c.cc.Invoke(ctx, "/proto.API/SearchFile", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *aPIClient) CollectArtifact(ctx context.Context, in *proto.ArtifactCollectorArgs, opts ...grpc.CallOption) (*proto.ArtifactCollectorResponse, error) { out := new(proto.ArtifactCollectorResponse) err := c.cc.Invoke(ctx, "/proto.API/CollectArtifact", in, out, opts...) @@ -347,9 +476,9 @@ func (c *aPIClient) CancelFlow(ctx context.Context, in *ApiFlowRequest, opts ... return out, nil } -func (c *aPIClient) ArchiveFlow(ctx context.Context, in *ApiFlowRequest, opts ...grpc.CallOption) (*StartFlowResponse, error) { - out := new(StartFlowResponse) - err := c.cc.Invoke(ctx, "/proto.API/ArchiveFlow", in, out, opts...) +func (c *aPIClient) ResumeFlow(ctx context.Context, in *ApiFlowRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/proto.API/ResumeFlow", in, out, opts...) if err != nil { return nil, err } @@ -374,7 +503,7 @@ func (c *aPIClient) GetFlowRequests(ctx context.Context, in *ApiFlowRequest, opt return out, nil } -func (c *aPIClient) GetKeywordCompletions(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*KeywordCompletions, error) { +func (c *aPIClient) GetKeywordCompletions(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*KeywordCompletions, error) { out := new(KeywordCompletions) err := c.cc.Invoke(ctx, "/proto.API/GetKeywordCompletions", in, out, opts...) if err != nil { @@ -383,6 +512,15 @@ func (c *aPIClient) GetKeywordCompletions(ctx context.Context, in *empty.Empty, return out, nil } +func (c *aPIClient) ReformatVQL(ctx context.Context, in *ReformatVQLMessage, opts ...grpc.CallOption) (*ReformatVQLMessage, error) { + out := new(ReformatVQLMessage) + err := c.cc.Invoke(ctx, "/proto.API/ReformatVQL", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *aPIClient) GetArtifacts(ctx context.Context, in *GetArtifactsRequest, opts ...grpc.CallOption) (*proto1.ArtifactDescriptors, error) { out := new(proto1.ArtifactDescriptors) err := c.cc.Invoke(ctx, "/proto.API/GetArtifacts", in, out, opts...) @@ -401,8 +539,8 @@ func (c *aPIClient) GetArtifactFile(ctx context.Context, in *GetArtifactRequest, return out, nil } -func (c *aPIClient) SetArtifactFile(ctx context.Context, in *SetArtifactRequest, opts ...grpc.CallOption) (*APIResponse, error) { - out := new(APIResponse) +func (c *aPIClient) SetArtifactFile(ctx context.Context, in *SetArtifactRequest, opts ...grpc.CallOption) (*SetArtifactResponse, error) { + out := new(SetArtifactResponse) err := c.cc.Invoke(ctx, "/proto.API/SetArtifactFile", in, out, opts...) if err != nil { return nil, err @@ -410,7 +548,7 @@ func (c *aPIClient) SetArtifactFile(ctx context.Context, in *SetArtifactRequest, return out, nil } -func (c *aPIClient) LoadArtifactPack(ctx context.Context, in *VFSFileBuffer, opts ...grpc.CallOption) (*LoadArtifactPackResponse, error) { +func (c *aPIClient) LoadArtifactPack(ctx context.Context, in *LoadArtifactPackRequest, opts ...grpc.CallOption) (*LoadArtifactPackResponse, error) { out := new(LoadArtifactPackResponse) err := c.cc.Invoke(ctx, "/proto.API/LoadArtifactPack", in, out, opts...) if err != nil { @@ -419,6 +557,15 @@ func (c *aPIClient) LoadArtifactPack(ctx context.Context, in *VFSFileBuffer, opt return out, nil } +func (c *aPIClient) SearchDocs(ctx context.Context, in *DocSearchRequest, opts ...grpc.CallOption) (*DocSearchResponses, error) { + out := new(DocSearchResponses) + err := c.cc.Invoke(ctx, "/proto.API/SearchDocs", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *aPIClient) GetToolInfo(ctx context.Context, in *proto1.Tool, opts ...grpc.CallOption) (*proto1.Tool, error) { out := new(proto1.Tool) err := c.cc.Invoke(ctx, "/proto.API/GetToolInfo", in, out, opts...) @@ -446,7 +593,7 @@ func (c *aPIClient) GetReport(ctx context.Context, in *GetReportRequest, opts .. return out, nil } -func (c *aPIClient) GetServerMonitoringState(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*proto.ArtifactCollectorArgs, error) { +func (c *aPIClient) GetServerMonitoringState(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*proto.ArtifactCollectorArgs, error) { out := new(proto.ArtifactCollectorArgs) err := c.cc.Invoke(ctx, "/proto.API/GetServerMonitoringState", in, out, opts...) if err != nil { @@ -473,8 +620,8 @@ func (c *aPIClient) GetClientMonitoringState(ctx context.Context, in *proto.GetC return out, nil } -func (c *aPIClient) SetClientMonitoringState(ctx context.Context, in *proto.ClientEventTable, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) +func (c *aPIClient) SetClientMonitoringState(ctx context.Context, in *proto.ClientEventTable, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/proto.API/SetClientMonitoringState", in, out, opts...) if err != nil { return nil, err @@ -527,6 +674,15 @@ func (c *aPIClient) UpdateNotebook(ctx context.Context, in *NotebookMetadata, op return out, nil } +func (c *aPIClient) DeleteNotebook(ctx context.Context, in *NotebookMetadata, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/proto.API/DeleteNotebook", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *aPIClient) NewNotebookCell(ctx context.Context, in *NotebookCellRequest, opts ...grpc.CallOption) (*NotebookMetadata, error) { out := new(NotebookMetadata) err := c.cc.Invoke(ctx, "/proto.API/NewNotebookCell", in, out, opts...) @@ -554,8 +710,17 @@ func (c *aPIClient) UpdateNotebookCell(ctx context.Context, in *NotebookCellRequ return out, nil } -func (c *aPIClient) CancelNotebookCell(ctx context.Context, in *NotebookCellRequest, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) +func (c *aPIClient) RevertNotebookCell(ctx context.Context, in *NotebookCellRequest, opts ...grpc.CallOption) (*NotebookCell, error) { + out := new(NotebookCell) + err := c.cc.Invoke(ctx, "/proto.API/RevertNotebookCell", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) CancelNotebookCell(ctx context.Context, in *NotebookCellRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/proto.API/CancelNotebookCell", in, out, opts...) if err != nil { return nil, err @@ -563,8 +728,8 @@ func (c *aPIClient) CancelNotebookCell(ctx context.Context, in *NotebookCellRequ return out, nil } -func (c *aPIClient) CreateNotebookDownloadFile(ctx context.Context, in *NotebookExportRequest, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) +func (c *aPIClient) CreateNotebookDownloadFile(ctx context.Context, in *NotebookExportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/proto.API/CreateNotebookDownloadFile", in, out, opts...) if err != nil { return nil, err @@ -581,9 +746,54 @@ func (c *aPIClient) UploadNotebookAttachment(ctx context.Context, in *NotebookFi return out, nil } -func (c *aPIClient) ExportNotebook(ctx context.Context, in *NotebookExportRequest, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) - err := c.cc.Invoke(ctx, "/proto.API/ExportNotebook", in, out, opts...) +func (c *aPIClient) RemoveNotebookAttachment(ctx context.Context, in *NotebookFileUploadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/proto.API/RemoveNotebookAttachment", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) AnnotateTimeline(ctx context.Context, in *AnnotationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/proto.API/AnnotateTimeline", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) GetSecretDefinitions(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SecretDefinitionList, error) { + out := new(SecretDefinitionList) + err := c.cc.Invoke(ctx, "/proto.API/GetSecretDefinitions", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) AddSecret(ctx context.Context, in *Secret, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/proto.API/AddSecret", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) ModifySecret(ctx context.Context, in *ModifySecretRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/proto.API/ModifySecret", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) GetSecret(ctx context.Context, in *Secret, opts ...grpc.CallOption) (*Secret, error) { + out := new(Secret) + err := c.cc.Invoke(ctx, "/proto.API/GetSecret", in, out, opts...) if err != nil { return nil, err } @@ -663,8 +873,8 @@ func (x *aPIWatchEventClient) Recv() (*EventResponse, error) { return m, nil } -func (c *aPIClient) PushEvents(ctx context.Context, in *PushEventRequest, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) +func (c *aPIClient) PushEvents(ctx context.Context, in *PushEventRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/proto.API/PushEvents", in, out, opts...) if err != nil { return nil, err @@ -672,8 +882,8 @@ func (c *aPIClient) PushEvents(ctx context.Context, in *PushEventRequest, opts . return out, nil } -func (c *aPIClient) WriteEvent(ctx context.Context, in *proto2.VQLResponse, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) +func (c *aPIClient) WriteEvent(ctx context.Context, in *proto2.VQLResponse, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/proto.API/WriteEvent", in, out, opts...) if err != nil { return nil, err @@ -681,6 +891,37 @@ func (c *aPIClient) WriteEvent(ctx context.Context, in *proto2.VQLResponse, opts return out, nil } +func (c *aPIClient) Scheduler(ctx context.Context, opts ...grpc.CallOption) (API_SchedulerClient, error) { + stream, err := c.cc.NewStream(ctx, &API_ServiceDesc.Streams[2], "/proto.API/Scheduler", opts...) + if err != nil { + return nil, err + } + x := &aPISchedulerClient{stream} + return x, nil +} + +type API_SchedulerClient interface { + Send(*ScheduleRequest) error + Recv() (*ScheduleResponse, error) + grpc.ClientStream +} + +type aPISchedulerClient struct { + grpc.ClientStream +} + +func (x *aPISchedulerClient) Send(m *ScheduleRequest) error { + return x.ClientStream.SendMsg(m) +} + +func (x *aPISchedulerClient) Recv() (*ScheduleResponse, error) { + m := new(ScheduleResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + func (c *aPIClient) GetSubject(ctx context.Context, in *DataRequest, opts ...grpc.CallOption) (*DataResponse, error) { out := new(DataResponse) err := c.cc.Invoke(ctx, "/proto.API/GetSubject", in, out, opts...) @@ -699,8 +940,8 @@ func (c *aPIClient) SetSubject(ctx context.Context, in *DataRequest, opts ...grp return out, nil } -func (c *aPIClient) DeleteSubject(ctx context.Context, in *DataRequest, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) +func (c *aPIClient) DeleteSubject(ctx context.Context, in *DataRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/proto.API/DeleteSubject", in, out, opts...) if err != nil { return nil, err @@ -734,44 +975,62 @@ type APIServer interface { CreateHunt(context.Context, *Hunt) (*StartFlowResponse, error) // Returns an estimate of the number of clients that might be // affected by a hunt. - EstimateHunt(context.Context, *Hunt) (*HuntStats, error) + EstimateHunt(context.Context, *HuntEstimateRequest) (*HuntStats, error) + GetHuntTable(context.Context, *GetTableRequest) (*GetTableResponse, error) + // Deprecated - hunts are now listed with GetHuntTable() ListHunts(context.Context, *ListHuntsRequest) (*ListHuntsResponse, error) GetHunt(context.Context, *GetHuntRequest) (*Hunt, error) - ModifyHunt(context.Context, *Hunt) (*empty.Empty, error) + GetHuntTags(context.Context, *emptypb.Empty) (*HuntTags, error) + ModifyHunt(context.Context, *HuntMutation) (*emptypb.Empty, error) GetHuntFlows(context.Context, *GetTableRequest) (*GetTableResponse, error) GetHuntResults(context.Context, *GetHuntResultsRequest) (*GetTableResponse, error) // Clients. - NotifyClients(context.Context, *NotificationRequest) (*empty.Empty, error) + NotifyClients(context.Context, *NotificationRequest) (*emptypb.Empty, error) LabelClients(context.Context, *LabelClientsRequest) (*APIResponse, error) ListClients(context.Context, *SearchClientsRequest) (*SearchClientsResponse, error) GetClient(context.Context, *GetClientRequest) (*ApiClient, error) GetClientMetadata(context.Context, *GetClientRequest) (*ClientMetadata, error) - SetClientMetadata(context.Context, *ClientMetadata) (*empty.Empty, error) - GetClientFlows(context.Context, *ApiFlowRequest) (*ApiFlowResponse, error) + SetClientMetadata(context.Context, *SetClientMetadataRequest) (*emptypb.Empty, error) + GetClientFlows(context.Context, *GetTableRequest) (*GetTableResponse, error) // Users - GetUserUITraits(context.Context, *empty.Empty) (*ApiGrrUser, error) - SetGUIOptions(context.Context, *SetGUIOptionsRequest) (*empty.Empty, error) + GetUserUITraits(context.Context, *emptypb.Empty) (*ApiUser, error) + SetGUIOptions(context.Context, *SetGUIOptionsRequest) (*SetGUIOptionsResponse, error) // List all the GUI users known on this server. - GetUsers(context.Context, *empty.Empty) (*Users, error) + GetUsers(context.Context, *emptypb.Empty) (*Users, error) + // List all the GUI users in orgs in which we are a member + GetGlobalUsers(context.Context, *emptypb.Empty) (*Users, error) + GetUserRoles(context.Context, *UserRequest) (*UserRoles, error) + SetUserRoles(context.Context, *UserRoles) (*emptypb.Empty, error) + GetUser(context.Context, *UserRequest) (*VelociraptorUser, error) + CreateUser(context.Context, *UpdateUserRequest) (*emptypb.Empty, error) GetUserFavorites(context.Context, *Favorite) (*Favorites, error) + SetPassword(context.Context, *SetPasswordRequest) (*emptypb.Empty, error) // VFS VFSListDirectory(context.Context, *VFSListRequest) (*VFSListResponse, error) + VFSListDirectoryFiles(context.Context, *GetTableRequest) (*GetTableResponse, error) VFSRefreshDirectory(context.Context, *VFSRefreshDirectoryRequest) (*proto.ArtifactCollectorResponse, error) VFSStatDirectory(context.Context, *VFSListRequest) (*VFSListResponse, error) VFSStatDownload(context.Context, *VFSStatDownloadRequest) (*proto.VFSDownloadInfo, error) + VFSDownloadFile(context.Context, *VFSStatDownloadRequest) (*StartFlowResponse, error) GetTable(context.Context, *GetTableRequest) (*GetTableResponse, error) + // Facilitate the HexEditor search API + SearchFile(context.Context, *SearchFileRequest) (*SearchFileResponse, error) // Flows CollectArtifact(context.Context, *proto.ArtifactCollectorArgs) (*proto.ArtifactCollectorResponse, error) CancelFlow(context.Context, *ApiFlowRequest) (*StartFlowResponse, error) - ArchiveFlow(context.Context, *ApiFlowRequest) (*StartFlowResponse, error) + ResumeFlow(context.Context, *ApiFlowRequest) (*emptypb.Empty, error) GetFlowDetails(context.Context, *ApiFlowRequest) (*FlowDetails, error) GetFlowRequests(context.Context, *ApiFlowRequest) (*ApiFlowRequestDetails, error) - GetKeywordCompletions(context.Context, *empty.Empty) (*KeywordCompletions, error) + // VQL assistance + GetKeywordCompletions(context.Context, *emptypb.Empty) (*KeywordCompletions, error) + ReformatVQL(context.Context, *ReformatVQLMessage) (*ReformatVQLMessage, error) // Artifacts GetArtifacts(context.Context, *GetArtifactsRequest) (*proto1.ArtifactDescriptors, error) GetArtifactFile(context.Context, *GetArtifactRequest) (*GetArtifactResponse, error) - SetArtifactFile(context.Context, *SetArtifactRequest) (*APIResponse, error) - LoadArtifactPack(context.Context, *VFSFileBuffer) (*LoadArtifactPackResponse, error) + SetArtifactFile(context.Context, *SetArtifactRequest) (*SetArtifactResponse, error) + LoadArtifactPack(context.Context, *LoadArtifactPackRequest) (*LoadArtifactPackResponse, error) + // Documentation + SearchDocs(context.Context, *DocSearchRequest) (*DocSearchResponses, error) // Tools GetToolInfo(context.Context, *proto1.Tool) (*proto1.Tool, error) SetToolInfo(context.Context, *proto1.Tool) (*proto1.Tool, error) @@ -779,7 +1038,7 @@ type APIServer interface { GetReport(context.Context, *GetReportRequest) (*GetReportResponse, error) // Server Monitoring Artifacts - manage the Server Monitoring // Service.. - GetServerMonitoringState(context.Context, *empty.Empty) (*proto.ArtifactCollectorArgs, error) + GetServerMonitoringState(context.Context, *emptypb.Empty) (*proto.ArtifactCollectorArgs, error) // Server Monitoring Artifacts - manage the Server Monitoring // Service. SetServerMonitoringState(context.Context, *proto.ArtifactCollectorArgs) (*proto.ArtifactCollectorArgs, error) @@ -788,7 +1047,7 @@ type APIServer interface { GetClientMonitoringState(context.Context, *proto.GetClientMonitoringStateRequest) (*proto.ClientEventTable, error) // Client Monitoring Artifacts - manage the Client Monitoring // Service. - SetClientMonitoringState(context.Context, *proto.ClientEventTable) (*empty.Empty, error) + SetClientMonitoringState(context.Context, *proto.ClientEventTable) (*emptypb.Empty, error) ListAvailableEventResults(context.Context, *ListAvailableEventResultsRequest) (*ListAvailableEventResultsResponse, error) // Schedule downloads. CreateDownloadFile(context.Context, *CreateDownloadRequest) (*CreateDownloadResponse, error) @@ -796,13 +1055,23 @@ type APIServer interface { GetNotebooks(context.Context, *NotebookCellRequest) (*Notebooks, error) NewNotebook(context.Context, *NotebookMetadata) (*NotebookMetadata, error) UpdateNotebook(context.Context, *NotebookMetadata) (*NotebookMetadata, error) + DeleteNotebook(context.Context, *NotebookMetadata) (*emptypb.Empty, error) NewNotebookCell(context.Context, *NotebookCellRequest) (*NotebookMetadata, error) GetNotebookCell(context.Context, *NotebookCellRequest) (*NotebookCell, error) UpdateNotebookCell(context.Context, *NotebookCellRequest) (*NotebookCell, error) - CancelNotebookCell(context.Context, *NotebookCellRequest) (*empty.Empty, error) - CreateNotebookDownloadFile(context.Context, *NotebookExportRequest) (*empty.Empty, error) + RevertNotebookCell(context.Context, *NotebookCellRequest) (*NotebookCell, error) + CancelNotebookCell(context.Context, *NotebookCellRequest) (*emptypb.Empty, error) + CreateNotebookDownloadFile(context.Context, *NotebookExportRequest) (*emptypb.Empty, error) UploadNotebookAttachment(context.Context, *NotebookFileUploadRequest) (*NotebookFileUploadResponse, error) - ExportNotebook(context.Context, *NotebookExportRequest) (*empty.Empty, error) + // Remove a notebook attachment. + RemoveNotebookAttachment(context.Context, *NotebookFileUploadRequest) (*emptypb.Empty, error) + AnnotateTimeline(context.Context, *AnnotationRequest) (*emptypb.Empty, error) + // Secret management + GetSecretDefinitions(context.Context, *emptypb.Empty) (*SecretDefinitionList, error) + AddSecret(context.Context, *Secret) (*emptypb.Empty, error) + ModifySecret(context.Context, *ModifySecretRequest) (*emptypb.Empty, error) + // Returns a redacted version of the secret. + GetSecret(context.Context, *Secret) (*Secret, error) // This can be used by API clients to fetch file content. VFSGetBuffer(context.Context, *VFSFileBuffer) (*VFSFileBuffer, error) // Streaming free form VQL. @@ -810,13 +1079,15 @@ type APIServer interface { // Watch for events from the master. WatchEvent(*EventRequest, API_WatchEventServer) error // Push the events to the master - PushEvents(context.Context, *PushEventRequest) (*empty.Empty, error) + PushEvents(context.Context, *PushEventRequest) (*emptypb.Empty, error) // Push monitoring event to the server. - WriteEvent(context.Context, *proto2.VQLResponse) (*empty.Empty, error) + WriteEvent(context.Context, *proto2.VQLResponse) (*emptypb.Empty, error) + // Scheduler endpoint for minion scheduling + Scheduler(API_SchedulerServer) error // Remote data store access. GetSubject(context.Context, *DataRequest) (*DataResponse, error) SetSubject(context.Context, *DataRequest) (*DataResponse, error) - DeleteSubject(context.Context, *DataRequest) (*empty.Empty, error) + DeleteSubject(context.Context, *DataRequest) (*emptypb.Empty, error) ListChildren(context.Context, *DataRequest) (*ListChildrenResponse, error) // Health check protocol as in https://github.com/grpc/grpc/blob/master/doc/health-checking.md Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) @@ -830,16 +1101,22 @@ type UnimplementedAPIServer struct { func (UnimplementedAPIServer) CreateHunt(context.Context, *Hunt) (*StartFlowResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateHunt not implemented") } -func (UnimplementedAPIServer) EstimateHunt(context.Context, *Hunt) (*HuntStats, error) { +func (UnimplementedAPIServer) EstimateHunt(context.Context, *HuntEstimateRequest) (*HuntStats, error) { return nil, status.Errorf(codes.Unimplemented, "method EstimateHunt not implemented") } +func (UnimplementedAPIServer) GetHuntTable(context.Context, *GetTableRequest) (*GetTableResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetHuntTable not implemented") +} func (UnimplementedAPIServer) ListHunts(context.Context, *ListHuntsRequest) (*ListHuntsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListHunts not implemented") } func (UnimplementedAPIServer) GetHunt(context.Context, *GetHuntRequest) (*Hunt, error) { return nil, status.Errorf(codes.Unimplemented, "method GetHunt not implemented") } -func (UnimplementedAPIServer) ModifyHunt(context.Context, *Hunt) (*empty.Empty, error) { +func (UnimplementedAPIServer) GetHuntTags(context.Context, *emptypb.Empty) (*HuntTags, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetHuntTags not implemented") +} +func (UnimplementedAPIServer) ModifyHunt(context.Context, *HuntMutation) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method ModifyHunt not implemented") } func (UnimplementedAPIServer) GetHuntFlows(context.Context, *GetTableRequest) (*GetTableResponse, error) { @@ -848,7 +1125,7 @@ func (UnimplementedAPIServer) GetHuntFlows(context.Context, *GetTableRequest) (* func (UnimplementedAPIServer) GetHuntResults(context.Context, *GetHuntResultsRequest) (*GetTableResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetHuntResults not implemented") } -func (UnimplementedAPIServer) NotifyClients(context.Context, *NotificationRequest) (*empty.Empty, error) { +func (UnimplementedAPIServer) NotifyClients(context.Context, *NotificationRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method NotifyClients not implemented") } func (UnimplementedAPIServer) LabelClients(context.Context, *LabelClientsRequest) (*APIResponse, error) { @@ -863,27 +1140,48 @@ func (UnimplementedAPIServer) GetClient(context.Context, *GetClientRequest) (*Ap func (UnimplementedAPIServer) GetClientMetadata(context.Context, *GetClientRequest) (*ClientMetadata, error) { return nil, status.Errorf(codes.Unimplemented, "method GetClientMetadata not implemented") } -func (UnimplementedAPIServer) SetClientMetadata(context.Context, *ClientMetadata) (*empty.Empty, error) { +func (UnimplementedAPIServer) SetClientMetadata(context.Context, *SetClientMetadataRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method SetClientMetadata not implemented") } -func (UnimplementedAPIServer) GetClientFlows(context.Context, *ApiFlowRequest) (*ApiFlowResponse, error) { +func (UnimplementedAPIServer) GetClientFlows(context.Context, *GetTableRequest) (*GetTableResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetClientFlows not implemented") } -func (UnimplementedAPIServer) GetUserUITraits(context.Context, *empty.Empty) (*ApiGrrUser, error) { +func (UnimplementedAPIServer) GetUserUITraits(context.Context, *emptypb.Empty) (*ApiUser, error) { return nil, status.Errorf(codes.Unimplemented, "method GetUserUITraits not implemented") } -func (UnimplementedAPIServer) SetGUIOptions(context.Context, *SetGUIOptionsRequest) (*empty.Empty, error) { +func (UnimplementedAPIServer) SetGUIOptions(context.Context, *SetGUIOptionsRequest) (*SetGUIOptionsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SetGUIOptions not implemented") } -func (UnimplementedAPIServer) GetUsers(context.Context, *empty.Empty) (*Users, error) { +func (UnimplementedAPIServer) GetUsers(context.Context, *emptypb.Empty) (*Users, error) { return nil, status.Errorf(codes.Unimplemented, "method GetUsers not implemented") } +func (UnimplementedAPIServer) GetGlobalUsers(context.Context, *emptypb.Empty) (*Users, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetGlobalUsers not implemented") +} +func (UnimplementedAPIServer) GetUserRoles(context.Context, *UserRequest) (*UserRoles, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUserRoles not implemented") +} +func (UnimplementedAPIServer) SetUserRoles(context.Context, *UserRoles) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetUserRoles not implemented") +} +func (UnimplementedAPIServer) GetUser(context.Context, *UserRequest) (*VelociraptorUser, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented") +} +func (UnimplementedAPIServer) CreateUser(context.Context, *UpdateUserRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") +} func (UnimplementedAPIServer) GetUserFavorites(context.Context, *Favorite) (*Favorites, error) { return nil, status.Errorf(codes.Unimplemented, "method GetUserFavorites not implemented") } +func (UnimplementedAPIServer) SetPassword(context.Context, *SetPasswordRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetPassword not implemented") +} func (UnimplementedAPIServer) VFSListDirectory(context.Context, *VFSListRequest) (*VFSListResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method VFSListDirectory not implemented") } +func (UnimplementedAPIServer) VFSListDirectoryFiles(context.Context, *GetTableRequest) (*GetTableResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VFSListDirectoryFiles not implemented") +} func (UnimplementedAPIServer) VFSRefreshDirectory(context.Context, *VFSRefreshDirectoryRequest) (*proto.ArtifactCollectorResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method VFSRefreshDirectory not implemented") } @@ -893,17 +1191,23 @@ func (UnimplementedAPIServer) VFSStatDirectory(context.Context, *VFSListRequest) func (UnimplementedAPIServer) VFSStatDownload(context.Context, *VFSStatDownloadRequest) (*proto.VFSDownloadInfo, error) { return nil, status.Errorf(codes.Unimplemented, "method VFSStatDownload not implemented") } +func (UnimplementedAPIServer) VFSDownloadFile(context.Context, *VFSStatDownloadRequest) (*StartFlowResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VFSDownloadFile not implemented") +} func (UnimplementedAPIServer) GetTable(context.Context, *GetTableRequest) (*GetTableResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetTable not implemented") } +func (UnimplementedAPIServer) SearchFile(context.Context, *SearchFileRequest) (*SearchFileResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SearchFile not implemented") +} func (UnimplementedAPIServer) CollectArtifact(context.Context, *proto.ArtifactCollectorArgs) (*proto.ArtifactCollectorResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CollectArtifact not implemented") } func (UnimplementedAPIServer) CancelFlow(context.Context, *ApiFlowRequest) (*StartFlowResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CancelFlow not implemented") } -func (UnimplementedAPIServer) ArchiveFlow(context.Context, *ApiFlowRequest) (*StartFlowResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ArchiveFlow not implemented") +func (UnimplementedAPIServer) ResumeFlow(context.Context, *ApiFlowRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method ResumeFlow not implemented") } func (UnimplementedAPIServer) GetFlowDetails(context.Context, *ApiFlowRequest) (*FlowDetails, error) { return nil, status.Errorf(codes.Unimplemented, "method GetFlowDetails not implemented") @@ -911,21 +1215,27 @@ func (UnimplementedAPIServer) GetFlowDetails(context.Context, *ApiFlowRequest) ( func (UnimplementedAPIServer) GetFlowRequests(context.Context, *ApiFlowRequest) (*ApiFlowRequestDetails, error) { return nil, status.Errorf(codes.Unimplemented, "method GetFlowRequests not implemented") } -func (UnimplementedAPIServer) GetKeywordCompletions(context.Context, *empty.Empty) (*KeywordCompletions, error) { +func (UnimplementedAPIServer) GetKeywordCompletions(context.Context, *emptypb.Empty) (*KeywordCompletions, error) { return nil, status.Errorf(codes.Unimplemented, "method GetKeywordCompletions not implemented") } +func (UnimplementedAPIServer) ReformatVQL(context.Context, *ReformatVQLMessage) (*ReformatVQLMessage, error) { + return nil, status.Errorf(codes.Unimplemented, "method ReformatVQL not implemented") +} func (UnimplementedAPIServer) GetArtifacts(context.Context, *GetArtifactsRequest) (*proto1.ArtifactDescriptors, error) { return nil, status.Errorf(codes.Unimplemented, "method GetArtifacts not implemented") } func (UnimplementedAPIServer) GetArtifactFile(context.Context, *GetArtifactRequest) (*GetArtifactResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetArtifactFile not implemented") } -func (UnimplementedAPIServer) SetArtifactFile(context.Context, *SetArtifactRequest) (*APIResponse, error) { +func (UnimplementedAPIServer) SetArtifactFile(context.Context, *SetArtifactRequest) (*SetArtifactResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SetArtifactFile not implemented") } -func (UnimplementedAPIServer) LoadArtifactPack(context.Context, *VFSFileBuffer) (*LoadArtifactPackResponse, error) { +func (UnimplementedAPIServer) LoadArtifactPack(context.Context, *LoadArtifactPackRequest) (*LoadArtifactPackResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method LoadArtifactPack not implemented") } +func (UnimplementedAPIServer) SearchDocs(context.Context, *DocSearchRequest) (*DocSearchResponses, error) { + return nil, status.Errorf(codes.Unimplemented, "method SearchDocs not implemented") +} func (UnimplementedAPIServer) GetToolInfo(context.Context, *proto1.Tool) (*proto1.Tool, error) { return nil, status.Errorf(codes.Unimplemented, "method GetToolInfo not implemented") } @@ -935,7 +1245,7 @@ func (UnimplementedAPIServer) SetToolInfo(context.Context, *proto1.Tool) (*proto func (UnimplementedAPIServer) GetReport(context.Context, *GetReportRequest) (*GetReportResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetReport not implemented") } -func (UnimplementedAPIServer) GetServerMonitoringState(context.Context, *empty.Empty) (*proto.ArtifactCollectorArgs, error) { +func (UnimplementedAPIServer) GetServerMonitoringState(context.Context, *emptypb.Empty) (*proto.ArtifactCollectorArgs, error) { return nil, status.Errorf(codes.Unimplemented, "method GetServerMonitoringState not implemented") } func (UnimplementedAPIServer) SetServerMonitoringState(context.Context, *proto.ArtifactCollectorArgs) (*proto.ArtifactCollectorArgs, error) { @@ -944,7 +1254,7 @@ func (UnimplementedAPIServer) SetServerMonitoringState(context.Context, *proto.A func (UnimplementedAPIServer) GetClientMonitoringState(context.Context, *proto.GetClientMonitoringStateRequest) (*proto.ClientEventTable, error) { return nil, status.Errorf(codes.Unimplemented, "method GetClientMonitoringState not implemented") } -func (UnimplementedAPIServer) SetClientMonitoringState(context.Context, *proto.ClientEventTable) (*empty.Empty, error) { +func (UnimplementedAPIServer) SetClientMonitoringState(context.Context, *proto.ClientEventTable) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method SetClientMonitoringState not implemented") } func (UnimplementedAPIServer) ListAvailableEventResults(context.Context, *ListAvailableEventResultsRequest) (*ListAvailableEventResultsResponse, error) { @@ -962,6 +1272,9 @@ func (UnimplementedAPIServer) NewNotebook(context.Context, *NotebookMetadata) (* func (UnimplementedAPIServer) UpdateNotebook(context.Context, *NotebookMetadata) (*NotebookMetadata, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateNotebook not implemented") } +func (UnimplementedAPIServer) DeleteNotebook(context.Context, *NotebookMetadata) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteNotebook not implemented") +} func (UnimplementedAPIServer) NewNotebookCell(context.Context, *NotebookCellRequest) (*NotebookMetadata, error) { return nil, status.Errorf(codes.Unimplemented, "method NewNotebookCell not implemented") } @@ -971,17 +1284,35 @@ func (UnimplementedAPIServer) GetNotebookCell(context.Context, *NotebookCellRequ func (UnimplementedAPIServer) UpdateNotebookCell(context.Context, *NotebookCellRequest) (*NotebookCell, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateNotebookCell not implemented") } -func (UnimplementedAPIServer) CancelNotebookCell(context.Context, *NotebookCellRequest) (*empty.Empty, error) { +func (UnimplementedAPIServer) RevertNotebookCell(context.Context, *NotebookCellRequest) (*NotebookCell, error) { + return nil, status.Errorf(codes.Unimplemented, "method RevertNotebookCell not implemented") +} +func (UnimplementedAPIServer) CancelNotebookCell(context.Context, *NotebookCellRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method CancelNotebookCell not implemented") } -func (UnimplementedAPIServer) CreateNotebookDownloadFile(context.Context, *NotebookExportRequest) (*empty.Empty, error) { +func (UnimplementedAPIServer) CreateNotebookDownloadFile(context.Context, *NotebookExportRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateNotebookDownloadFile not implemented") } func (UnimplementedAPIServer) UploadNotebookAttachment(context.Context, *NotebookFileUploadRequest) (*NotebookFileUploadResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UploadNotebookAttachment not implemented") } -func (UnimplementedAPIServer) ExportNotebook(context.Context, *NotebookExportRequest) (*empty.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method ExportNotebook not implemented") +func (UnimplementedAPIServer) RemoveNotebookAttachment(context.Context, *NotebookFileUploadRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveNotebookAttachment not implemented") +} +func (UnimplementedAPIServer) AnnotateTimeline(context.Context, *AnnotationRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method AnnotateTimeline not implemented") +} +func (UnimplementedAPIServer) GetSecretDefinitions(context.Context, *emptypb.Empty) (*SecretDefinitionList, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSecretDefinitions not implemented") +} +func (UnimplementedAPIServer) AddSecret(context.Context, *Secret) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddSecret not implemented") +} +func (UnimplementedAPIServer) ModifySecret(context.Context, *ModifySecretRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method ModifySecret not implemented") +} +func (UnimplementedAPIServer) GetSecret(context.Context, *Secret) (*Secret, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSecret not implemented") } func (UnimplementedAPIServer) VFSGetBuffer(context.Context, *VFSFileBuffer) (*VFSFileBuffer, error) { return nil, status.Errorf(codes.Unimplemented, "method VFSGetBuffer not implemented") @@ -992,19 +1323,22 @@ func (UnimplementedAPIServer) Query(*proto2.VQLCollectorArgs, API_QueryServer) e func (UnimplementedAPIServer) WatchEvent(*EventRequest, API_WatchEventServer) error { return status.Errorf(codes.Unimplemented, "method WatchEvent not implemented") } -func (UnimplementedAPIServer) PushEvents(context.Context, *PushEventRequest) (*empty.Empty, error) { +func (UnimplementedAPIServer) PushEvents(context.Context, *PushEventRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method PushEvents not implemented") } -func (UnimplementedAPIServer) WriteEvent(context.Context, *proto2.VQLResponse) (*empty.Empty, error) { +func (UnimplementedAPIServer) WriteEvent(context.Context, *proto2.VQLResponse) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method WriteEvent not implemented") } +func (UnimplementedAPIServer) Scheduler(API_SchedulerServer) error { + return status.Errorf(codes.Unimplemented, "method Scheduler not implemented") +} func (UnimplementedAPIServer) GetSubject(context.Context, *DataRequest) (*DataResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetSubject not implemented") } func (UnimplementedAPIServer) SetSubject(context.Context, *DataRequest) (*DataResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SetSubject not implemented") } -func (UnimplementedAPIServer) DeleteSubject(context.Context, *DataRequest) (*empty.Empty, error) { +func (UnimplementedAPIServer) DeleteSubject(context.Context, *DataRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteSubject not implemented") } func (UnimplementedAPIServer) ListChildren(context.Context, *DataRequest) (*ListChildrenResponse, error) { @@ -1045,7 +1379,7 @@ func _API_CreateHunt_Handler(srv interface{}, ctx context.Context, dec func(inte } func _API_EstimateHunt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Hunt) + in := new(HuntEstimateRequest) if err := dec(in); err != nil { return nil, err } @@ -1057,7 +1391,25 @@ func _API_EstimateHunt_Handler(srv interface{}, ctx context.Context, dec func(in FullMethod: "/proto.API/EstimateHunt", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(APIServer).EstimateHunt(ctx, req.(*Hunt)) + return srv.(APIServer).EstimateHunt(ctx, req.(*HuntEstimateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_GetHuntTable_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTableRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).GetHuntTable(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/GetHuntTable", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).GetHuntTable(ctx, req.(*GetTableRequest)) } return interceptor(ctx, in, info, handler) } @@ -1098,8 +1450,26 @@ func _API_GetHunt_Handler(srv interface{}, ctx context.Context, dec func(interfa return interceptor(ctx, in, info, handler) } +func _API_GetHuntTags_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).GetHuntTags(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/GetHuntTags", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).GetHuntTags(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _API_ModifyHunt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Hunt) + in := new(HuntMutation) if err := dec(in); err != nil { return nil, err } @@ -1111,7 +1481,7 @@ func _API_ModifyHunt_Handler(srv interface{}, ctx context.Context, dec func(inte FullMethod: "/proto.API/ModifyHunt", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(APIServer).ModifyHunt(ctx, req.(*Hunt)) + return srv.(APIServer).ModifyHunt(ctx, req.(*HuntMutation)) } return interceptor(ctx, in, info, handler) } @@ -1243,7 +1613,7 @@ func _API_GetClientMetadata_Handler(srv interface{}, ctx context.Context, dec fu } func _API_SetClientMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ClientMetadata) + in := new(SetClientMetadataRequest) if err := dec(in); err != nil { return nil, err } @@ -1255,13 +1625,13 @@ func _API_SetClientMetadata_Handler(srv interface{}, ctx context.Context, dec fu FullMethod: "/proto.API/SetClientMetadata", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(APIServer).SetClientMetadata(ctx, req.(*ClientMetadata)) + return srv.(APIServer).SetClientMetadata(ctx, req.(*SetClientMetadataRequest)) } return interceptor(ctx, in, info, handler) } func _API_GetClientFlows_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ApiFlowRequest) + in := new(GetTableRequest) if err := dec(in); err != nil { return nil, err } @@ -1273,13 +1643,13 @@ func _API_GetClientFlows_Handler(srv interface{}, ctx context.Context, dec func( FullMethod: "/proto.API/GetClientFlows", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(APIServer).GetClientFlows(ctx, req.(*ApiFlowRequest)) + return srv.(APIServer).GetClientFlows(ctx, req.(*GetTableRequest)) } return interceptor(ctx, in, info, handler) } func _API_GetUserUITraits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(empty.Empty) + in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } @@ -1291,7 +1661,7 @@ func _API_GetUserUITraits_Handler(srv interface{}, ctx context.Context, dec func FullMethod: "/proto.API/GetUserUITraits", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(APIServer).GetUserUITraits(ctx, req.(*empty.Empty)) + return srv.(APIServer).GetUserUITraits(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } @@ -1315,7 +1685,7 @@ func _API_SetGUIOptions_Handler(srv interface{}, ctx context.Context, dec func(i } func _API_GetUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(empty.Empty) + in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } @@ -1327,7 +1697,97 @@ func _API_GetUsers_Handler(srv interface{}, ctx context.Context, dec func(interf FullMethod: "/proto.API/GetUsers", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(APIServer).GetUsers(ctx, req.(*empty.Empty)) + return srv.(APIServer).GetUsers(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_GetGlobalUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).GetGlobalUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/GetGlobalUsers", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).GetGlobalUsers(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_GetUserRoles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).GetUserRoles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/GetUserRoles", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).GetUserRoles(ctx, req.(*UserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_SetUserRoles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserRoles) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).SetUserRoles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/SetUserRoles", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).SetUserRoles(ctx, req.(*UserRoles)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).GetUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/GetUser", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).GetUser(ctx, req.(*UserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).CreateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/CreateUser", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).CreateUser(ctx, req.(*UpdateUserRequest)) } return interceptor(ctx, in, info, handler) } @@ -1350,6 +1810,24 @@ func _API_GetUserFavorites_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _API_SetPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetPasswordRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).SetPassword(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/SetPassword", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).SetPassword(ctx, req.(*SetPasswordRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _API_VFSListDirectory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(VFSListRequest) if err := dec(in); err != nil { @@ -1368,6 +1846,24 @@ func _API_VFSListDirectory_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _API_VFSListDirectoryFiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTableRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).VFSListDirectoryFiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/VFSListDirectoryFiles", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).VFSListDirectoryFiles(ctx, req.(*GetTableRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _API_VFSRefreshDirectory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(VFSRefreshDirectoryRequest) if err := dec(in); err != nil { @@ -1422,6 +1918,24 @@ func _API_VFSStatDownload_Handler(srv interface{}, ctx context.Context, dec func return interceptor(ctx, in, info, handler) } +func _API_VFSDownloadFile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VFSStatDownloadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).VFSDownloadFile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/VFSDownloadFile", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).VFSDownloadFile(ctx, req.(*VFSStatDownloadRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _API_GetTable_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetTableRequest) if err := dec(in); err != nil { @@ -1440,6 +1954,24 @@ func _API_GetTable_Handler(srv interface{}, ctx context.Context, dec func(interf return interceptor(ctx, in, info, handler) } +func _API_SearchFile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SearchFileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).SearchFile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/SearchFile", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).SearchFile(ctx, req.(*SearchFileRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _API_CollectArtifact_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(proto.ArtifactCollectorArgs) if err := dec(in); err != nil { @@ -1476,20 +2008,20 @@ func _API_CancelFlow_Handler(srv interface{}, ctx context.Context, dec func(inte return interceptor(ctx, in, info, handler) } -func _API_ArchiveFlow_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _API_ResumeFlow_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ApiFlowRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(APIServer).ArchiveFlow(ctx, in) + return srv.(APIServer).ResumeFlow(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/proto.API/ArchiveFlow", + FullMethod: "/proto.API/ResumeFlow", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(APIServer).ArchiveFlow(ctx, req.(*ApiFlowRequest)) + return srv.(APIServer).ResumeFlow(ctx, req.(*ApiFlowRequest)) } return interceptor(ctx, in, info, handler) } @@ -1531,7 +2063,7 @@ func _API_GetFlowRequests_Handler(srv interface{}, ctx context.Context, dec func } func _API_GetKeywordCompletions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(empty.Empty) + in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } @@ -1543,7 +2075,25 @@ func _API_GetKeywordCompletions_Handler(srv interface{}, ctx context.Context, de FullMethod: "/proto.API/GetKeywordCompletions", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(APIServer).GetKeywordCompletions(ctx, req.(*empty.Empty)) + return srv.(APIServer).GetKeywordCompletions(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_ReformatVQL_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReformatVQLMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).ReformatVQL(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/ReformatVQL", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).ReformatVQL(ctx, req.(*ReformatVQLMessage)) } return interceptor(ctx, in, info, handler) } @@ -1603,7 +2153,7 @@ func _API_SetArtifactFile_Handler(srv interface{}, ctx context.Context, dec func } func _API_LoadArtifactPack_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(VFSFileBuffer) + in := new(LoadArtifactPackRequest) if err := dec(in); err != nil { return nil, err } @@ -1615,7 +2165,25 @@ func _API_LoadArtifactPack_Handler(srv interface{}, ctx context.Context, dec fun FullMethod: "/proto.API/LoadArtifactPack", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(APIServer).LoadArtifactPack(ctx, req.(*VFSFileBuffer)) + return srv.(APIServer).LoadArtifactPack(ctx, req.(*LoadArtifactPackRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_SearchDocs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DocSearchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).SearchDocs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/SearchDocs", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).SearchDocs(ctx, req.(*DocSearchRequest)) } return interceptor(ctx, in, info, handler) } @@ -1675,7 +2243,7 @@ func _API_GetReport_Handler(srv interface{}, ctx context.Context, dec func(inter } func _API_GetServerMonitoringState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(empty.Empty) + in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } @@ -1687,7 +2255,7 @@ func _API_GetServerMonitoringState_Handler(srv interface{}, ctx context.Context, FullMethod: "/proto.API/GetServerMonitoringState", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(APIServer).GetServerMonitoringState(ctx, req.(*empty.Empty)) + return srv.(APIServer).GetServerMonitoringState(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } @@ -1836,6 +2404,24 @@ func _API_UpdateNotebook_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _API_DeleteNotebook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NotebookMetadata) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).DeleteNotebook(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/DeleteNotebook", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).DeleteNotebook(ctx, req.(*NotebookMetadata)) + } + return interceptor(ctx, in, info, handler) +} + func _API_NewNotebookCell_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(NotebookCellRequest) if err := dec(in); err != nil { @@ -1890,6 +2476,24 @@ func _API_UpdateNotebookCell_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _API_RevertNotebookCell_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NotebookCellRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).RevertNotebookCell(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/RevertNotebookCell", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).RevertNotebookCell(ctx, req.(*NotebookCellRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _API_CancelNotebookCell_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(NotebookCellRequest) if err := dec(in); err != nil { @@ -1944,20 +2548,110 @@ func _API_UploadNotebookAttachment_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } -func _API_ExportNotebook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(NotebookExportRequest) +func _API_RemoveNotebookAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NotebookFileUploadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).RemoveNotebookAttachment(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/RemoveNotebookAttachment", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).RemoveNotebookAttachment(ctx, req.(*NotebookFileUploadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_AnnotateTimeline_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AnnotationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).AnnotateTimeline(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/AnnotateTimeline", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).AnnotateTimeline(ctx, req.(*AnnotationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_GetSecretDefinitions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(APIServer).ExportNotebook(ctx, in) + return srv.(APIServer).GetSecretDefinitions(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/proto.API/ExportNotebook", + FullMethod: "/proto.API/GetSecretDefinitions", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(APIServer).ExportNotebook(ctx, req.(*NotebookExportRequest)) + return srv.(APIServer).GetSecretDefinitions(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_AddSecret_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Secret) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).AddSecret(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/AddSecret", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).AddSecret(ctx, req.(*Secret)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_ModifySecret_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ModifySecretRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).ModifySecret(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/ModifySecret", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).ModifySecret(ctx, req.(*ModifySecretRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_GetSecret_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Secret) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).GetSecret(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.API/GetSecret", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).GetSecret(ctx, req.(*Secret)) } return interceptor(ctx, in, info, handler) } @@ -2058,6 +2752,32 @@ func _API_WriteEvent_Handler(srv interface{}, ctx context.Context, dec func(inte return interceptor(ctx, in, info, handler) } +func _API_Scheduler_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(APIServer).Scheduler(&aPISchedulerServer{stream}) +} + +type API_SchedulerServer interface { + Send(*ScheduleResponse) error + Recv() (*ScheduleRequest, error) + grpc.ServerStream +} + +type aPISchedulerServer struct { + grpc.ServerStream +} + +func (x *aPISchedulerServer) Send(m *ScheduleResponse) error { + return x.ServerStream.SendMsg(m) +} + +func (x *aPISchedulerServer) Recv() (*ScheduleRequest, error) { + m := new(ScheduleRequest) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + func _API_GetSubject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DataRequest) if err := dec(in); err != nil { @@ -2163,6 +2883,10 @@ var API_ServiceDesc = grpc.ServiceDesc{ MethodName: "EstimateHunt", Handler: _API_EstimateHunt_Handler, }, + { + MethodName: "GetHuntTable", + Handler: _API_GetHuntTable_Handler, + }, { MethodName: "ListHunts", Handler: _API_ListHunts_Handler, @@ -2171,6 +2895,10 @@ var API_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetHunt", Handler: _API_GetHunt_Handler, }, + { + MethodName: "GetHuntTags", + Handler: _API_GetHuntTags_Handler, + }, { MethodName: "ModifyHunt", Handler: _API_ModifyHunt_Handler, @@ -2223,14 +2951,42 @@ var API_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetUsers", Handler: _API_GetUsers_Handler, }, + { + MethodName: "GetGlobalUsers", + Handler: _API_GetGlobalUsers_Handler, + }, + { + MethodName: "GetUserRoles", + Handler: _API_GetUserRoles_Handler, + }, + { + MethodName: "SetUserRoles", + Handler: _API_SetUserRoles_Handler, + }, + { + MethodName: "GetUser", + Handler: _API_GetUser_Handler, + }, + { + MethodName: "CreateUser", + Handler: _API_CreateUser_Handler, + }, { MethodName: "GetUserFavorites", Handler: _API_GetUserFavorites_Handler, }, + { + MethodName: "SetPassword", + Handler: _API_SetPassword_Handler, + }, { MethodName: "VFSListDirectory", Handler: _API_VFSListDirectory_Handler, }, + { + MethodName: "VFSListDirectoryFiles", + Handler: _API_VFSListDirectoryFiles_Handler, + }, { MethodName: "VFSRefreshDirectory", Handler: _API_VFSRefreshDirectory_Handler, @@ -2243,10 +2999,18 @@ var API_ServiceDesc = grpc.ServiceDesc{ MethodName: "VFSStatDownload", Handler: _API_VFSStatDownload_Handler, }, + { + MethodName: "VFSDownloadFile", + Handler: _API_VFSDownloadFile_Handler, + }, { MethodName: "GetTable", Handler: _API_GetTable_Handler, }, + { + MethodName: "SearchFile", + Handler: _API_SearchFile_Handler, + }, { MethodName: "CollectArtifact", Handler: _API_CollectArtifact_Handler, @@ -2256,8 +3020,8 @@ var API_ServiceDesc = grpc.ServiceDesc{ Handler: _API_CancelFlow_Handler, }, { - MethodName: "ArchiveFlow", - Handler: _API_ArchiveFlow_Handler, + MethodName: "ResumeFlow", + Handler: _API_ResumeFlow_Handler, }, { MethodName: "GetFlowDetails", @@ -2271,6 +3035,10 @@ var API_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetKeywordCompletions", Handler: _API_GetKeywordCompletions_Handler, }, + { + MethodName: "ReformatVQL", + Handler: _API_ReformatVQL_Handler, + }, { MethodName: "GetArtifacts", Handler: _API_GetArtifacts_Handler, @@ -2287,6 +3055,10 @@ var API_ServiceDesc = grpc.ServiceDesc{ MethodName: "LoadArtifactPack", Handler: _API_LoadArtifactPack_Handler, }, + { + MethodName: "SearchDocs", + Handler: _API_SearchDocs_Handler, + }, { MethodName: "GetToolInfo", Handler: _API_GetToolInfo_Handler, @@ -2335,6 +3107,10 @@ var API_ServiceDesc = grpc.ServiceDesc{ MethodName: "UpdateNotebook", Handler: _API_UpdateNotebook_Handler, }, + { + MethodName: "DeleteNotebook", + Handler: _API_DeleteNotebook_Handler, + }, { MethodName: "NewNotebookCell", Handler: _API_NewNotebookCell_Handler, @@ -2347,6 +3123,10 @@ var API_ServiceDesc = grpc.ServiceDesc{ MethodName: "UpdateNotebookCell", Handler: _API_UpdateNotebookCell_Handler, }, + { + MethodName: "RevertNotebookCell", + Handler: _API_RevertNotebookCell_Handler, + }, { MethodName: "CancelNotebookCell", Handler: _API_CancelNotebookCell_Handler, @@ -2360,8 +3140,28 @@ var API_ServiceDesc = grpc.ServiceDesc{ Handler: _API_UploadNotebookAttachment_Handler, }, { - MethodName: "ExportNotebook", - Handler: _API_ExportNotebook_Handler, + MethodName: "RemoveNotebookAttachment", + Handler: _API_RemoveNotebookAttachment_Handler, + }, + { + MethodName: "AnnotateTimeline", + Handler: _API_AnnotateTimeline_Handler, + }, + { + MethodName: "GetSecretDefinitions", + Handler: _API_GetSecretDefinitions_Handler, + }, + { + MethodName: "AddSecret", + Handler: _API_AddSecret_Handler, + }, + { + MethodName: "ModifySecret", + Handler: _API_ModifySecret_Handler, + }, + { + MethodName: "GetSecret", + Handler: _API_GetSecret_Handler, }, { MethodName: "VFSGetBuffer", @@ -2407,6 +3207,12 @@ var API_ServiceDesc = grpc.ServiceDesc{ Handler: _API_WatchEvent_Handler, ServerStreams: true, }, + { + StreamName: "Scheduler", + Handler: _API_Scheduler_Handler, + ServerStreams: true, + ClientStreams: true, + }, }, Metadata: "api.proto", } diff --git a/api/proto/artifacts.pb.go b/api/proto/artifacts.pb.go index f00863dc7..015123312 100644 --- a/api/proto/artifacts.pb.go +++ b/api/proto/artifacts.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: artifacts.proto package proto @@ -28,6 +25,9 @@ type SetArtifactRequest_Operation int32 const ( SetArtifactRequest_SET SetArtifactRequest_Operation = 0 SetArtifactRequest_DELETE SetArtifactRequest_Operation = 1 + SetArtifactRequest_CHECK SetArtifactRequest_Operation = 2 + // Only set the artifact if there are no errors or warnings. + SetArtifactRequest_CHECK_AND_SET SetArtifactRequest_Operation = 3 ) // Enum value maps for SetArtifactRequest_Operation. @@ -35,10 +35,14 @@ var ( SetArtifactRequest_Operation_name = map[int32]string{ 0: "SET", 1: "DELETE", + 2: "CHECK", + 3: "CHECK_AND_SET", } SetArtifactRequest_Operation_value = map[string]int32{ - "SET": 0, - "DELETE": 1, + "SET": 0, + "DELETE": 1, + "CHECK": 2, + "CHECK_AND_SET": 3, } ) @@ -74,7 +78,11 @@ type FieldSelector struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name bool `protobuf:"varint,1,opt,name=name,proto3" json:"name,omitempty"` + Name bool `protobuf:"varint,1,opt,name=name,proto3" json:"name,omitempty"` + Description bool `protobuf:"varint,2,opt,name=description,proto3" json:"description,omitempty"` + Type bool `protobuf:"varint,3,opt,name=type,proto3" json:"type,omitempty"` + Sources bool `protobuf:"varint,4,opt,name=sources,proto3" json:"sources,omitempty"` + Tags bool `protobuf:"varint,5,opt,name=tags,proto3" json:"tags,omitempty"` } func (x *FieldSelector) Reset() { @@ -116,6 +124,34 @@ func (x *FieldSelector) GetName() bool { return false } +func (x *FieldSelector) GetDescription() bool { + if x != nil { + return x.Description + } + return false +} + +func (x *FieldSelector) GetType() bool { + if x != nil { + return x.Type + } + return false +} + +func (x *FieldSelector) GetSources() bool { + if x != nil { + return x.Sources + } + return false +} + +func (x *FieldSelector) GetTags() bool { + if x != nil { + return x.Tags + } + return false +} + type GetArtifactsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -226,10 +262,6 @@ type GetArtifactRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Deprecated. - // string vfs_path = 1 [(sem_type) = { - // description: "The vfs path relative to the artifacts definition store." - // }]; Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` } @@ -324,8 +356,10 @@ type SetArtifactRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Artifact string `protobuf:"bytes,2,opt,name=artifact,proto3" json:"artifact,omitempty"` - Op SetArtifactRequest_Operation `protobuf:"varint,3,opt,name=op,proto3,enum=proto.SetArtifactRequest_Operation" json:"op,omitempty"` + Artifact string `protobuf:"bytes,2,opt,name=artifact,proto3" json:"artifact,omitempty"` + // Also set these tags. + Tags []string `protobuf:"bytes,4,rep,name=tags,proto3" json:"tags,omitempty"` + Op SetArtifactRequest_Operation `protobuf:"varint,3,opt,name=op,proto3,enum=proto.SetArtifactRequest_Operation" json:"op,omitempty"` } func (x *SetArtifactRequest) Reset() { @@ -367,6 +401,13 @@ func (x *SetArtifactRequest) GetArtifact() string { return "" } +func (x *SetArtifactRequest) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + func (x *SetArtifactRequest) GetOp() SetArtifactRequest_Operation { if x != nil { return x.Op @@ -374,6 +415,77 @@ func (x *SetArtifactRequest) GetOp() SetArtifactRequest_Operation { return SetArtifactRequest_SET } +type SetArtifactResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Error bool `protobuf:"varint,1,opt,name=error,proto3" json:"error,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + Errors []string `protobuf:"bytes,3,rep,name=errors,proto3" json:"errors,omitempty"` + Warnings []string `protobuf:"bytes,4,rep,name=warnings,proto3" json:"warnings,omitempty"` +} + +func (x *SetArtifactResponse) Reset() { + *x = SetArtifactResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_artifacts_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetArtifactResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetArtifactResponse) ProtoMessage() {} + +func (x *SetArtifactResponse) ProtoReflect() protoreflect.Message { + mi := &file_artifacts_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetArtifactResponse.ProtoReflect.Descriptor instead. +func (*SetArtifactResponse) Descriptor() ([]byte, []int) { + return file_artifacts_proto_rawDescGZIP(), []int{5} +} + +func (x *SetArtifactResponse) GetError() bool { + if x != nil { + return x.Error + } + return false +} + +func (x *SetArtifactResponse) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *SetArtifactResponse) GetErrors() []string { + if x != nil { + return x.Errors + } + return nil +} + +func (x *SetArtifactResponse) GetWarnings() []string { + if x != nil { + return x.Warnings + } + return nil +} + type LoadArtifactError struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -386,7 +498,7 @@ type LoadArtifactError struct { func (x *LoadArtifactError) Reset() { *x = LoadArtifactError{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[5] + mi := &file_artifacts_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -399,7 +511,7 @@ func (x *LoadArtifactError) String() string { func (*LoadArtifactError) ProtoMessage() {} func (x *LoadArtifactError) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[5] + mi := &file_artifacts_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -412,7 +524,7 @@ func (x *LoadArtifactError) ProtoReflect() protoreflect.Message { // Deprecated: Use LoadArtifactError.ProtoReflect.Descriptor instead. func (*LoadArtifactError) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{5} + return file_artifacts_proto_rawDescGZIP(), []int{6} } func (x *LoadArtifactError) GetFilename() string { @@ -429,19 +541,108 @@ func (x *LoadArtifactError) GetError() string { return "" } +type LoadArtifactPackRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` + Tags []string `protobuf:"bytes,6,rep,name=tags,proto3" json:"tags,omitempty"` + Filter string `protobuf:"bytes,2,opt,name=filter,proto3" json:"filter,omitempty"` + // NOTE: the vfs path must be in the VFS temp directory. + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + VfsPath []string `protobuf:"bytes,5,rep,name=vfs_path,json=vfsPath,proto3" json:"vfs_path,omitempty"` + ReallyDoIt bool `protobuf:"varint,4,opt,name=really_do_it,json=reallyDoIt,proto3" json:"really_do_it,omitempty"` +} + +func (x *LoadArtifactPackRequest) Reset() { + *x = LoadArtifactPackRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_artifacts_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LoadArtifactPackRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoadArtifactPackRequest) ProtoMessage() {} + +func (x *LoadArtifactPackRequest) ProtoReflect() protoreflect.Message { + mi := &file_artifacts_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoadArtifactPackRequest.ProtoReflect.Descriptor instead. +func (*LoadArtifactPackRequest) Descriptor() ([]byte, []int) { + return file_artifacts_proto_rawDescGZIP(), []int{7} +} + +func (x *LoadArtifactPackRequest) GetPrefix() string { + if x != nil { + return x.Prefix + } + return "" +} + +func (x *LoadArtifactPackRequest) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *LoadArtifactPackRequest) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +func (x *LoadArtifactPackRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *LoadArtifactPackRequest) GetVfsPath() []string { + if x != nil { + return x.VfsPath + } + return nil +} + +func (x *LoadArtifactPackRequest) GetReallyDoIt() bool { + if x != nil { + return x.ReallyDoIt + } + return false +} + type LoadArtifactPackResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields SuccessfulArtifacts []string `protobuf:"bytes,1,rep,name=successful_artifacts,json=successfulArtifacts,proto3" json:"successful_artifacts,omitempty"` + VfsPath []string `protobuf:"bytes,3,rep,name=vfs_path,json=vfsPath,proto3" json:"vfs_path,omitempty"` Errors []*LoadArtifactError `protobuf:"bytes,2,rep,name=errors,proto3" json:"errors,omitempty"` } func (x *LoadArtifactPackResponse) Reset() { *x = LoadArtifactPackResponse{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[6] + mi := &file_artifacts_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -454,7 +655,7 @@ func (x *LoadArtifactPackResponse) String() string { func (*LoadArtifactPackResponse) ProtoMessage() {} func (x *LoadArtifactPackResponse) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[6] + mi := &file_artifacts_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -467,7 +668,7 @@ func (x *LoadArtifactPackResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LoadArtifactPackResponse.ProtoReflect.Descriptor instead. func (*LoadArtifactPackResponse) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{6} + return file_artifacts_proto_rawDescGZIP(), []int{8} } func (x *LoadArtifactPackResponse) GetSuccessfulArtifacts() []string { @@ -477,6 +678,13 @@ func (x *LoadArtifactPackResponse) GetSuccessfulArtifacts() []string { return nil } +func (x *LoadArtifactPackResponse) GetVfsPath() []string { + if x != nil { + return x.VfsPath + } + return nil +} + func (x *LoadArtifactPackResponse) GetErrors() []*LoadArtifactError { if x != nil { return x.Errors @@ -496,7 +704,7 @@ type APIResponse struct { func (x *APIResponse) Reset() { *x = APIResponse{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[7] + mi := &file_artifacts_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -509,7 +717,7 @@ func (x *APIResponse) String() string { func (*APIResponse) ProtoMessage() {} func (x *APIResponse) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[7] + mi := &file_artifacts_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -522,7 +730,7 @@ func (x *APIResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use APIResponse.ProtoReflect.Descriptor instead. func (*APIResponse) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{7} + return file_artifacts_proto_rawDescGZIP(), []int{9} } func (x *APIResponse) GetError() bool { @@ -563,7 +771,7 @@ type GetReportRequest struct { func (x *GetReportRequest) Reset() { *x = GetReportRequest{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[8] + mi := &file_artifacts_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -576,7 +784,7 @@ func (x *GetReportRequest) String() string { func (*GetReportRequest) ProtoMessage() {} func (x *GetReportRequest) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[8] + mi := &file_artifacts_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -589,7 +797,7 @@ func (x *GetReportRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetReportRequest.ProtoReflect.Descriptor instead. func (*GetReportRequest) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{8} + return file_artifacts_proto_rawDescGZIP(), []int{10} } func (x *GetReportRequest) GetArtifact() string { @@ -679,7 +887,7 @@ type GetReportResponse struct { func (x *GetReportResponse) Reset() { *x = GetReportResponse{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[9] + mi := &file_artifacts_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -692,7 +900,7 @@ func (x *GetReportResponse) String() string { func (*GetReportResponse) ProtoMessage() {} func (x *GetReportResponse) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[9] + mi := &file_artifacts_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -705,7 +913,7 @@ func (x *GetReportResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetReportResponse.ProtoReflect.Descriptor instead. func (*GetReportResponse) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{9} + return file_artifacts_proto_rawDescGZIP(), []int{11} } func (x *GetReportResponse) GetData() string { @@ -739,7 +947,7 @@ type ArtifactCompressionDict struct { func (x *ArtifactCompressionDict) Reset() { *x = ArtifactCompressionDict{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[10] + mi := &file_artifacts_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -752,7 +960,7 @@ func (x *ArtifactCompressionDict) String() string { func (*ArtifactCompressionDict) ProtoMessage() {} func (x *ArtifactCompressionDict) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[10] + mi := &file_artifacts_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -765,7 +973,7 @@ func (x *ArtifactCompressionDict) ProtoReflect() protoreflect.Message { // Deprecated: Use ArtifactCompressionDict.ProtoReflect.Descriptor instead. func (*ArtifactCompressionDict) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{10} + return file_artifacts_proto_rawDescGZIP(), []int{12} } type ListAvailableEventResultsRequest struct { @@ -779,12 +987,13 @@ type ListAvailableEventResultsRequest struct { Artifact string `protobuf:"bytes,2,opt,name=artifact,proto3" json:"artifact,omitempty"` // This can be empty or "logs" to list the logs. LogType string `protobuf:"bytes,3,opt,name=log_type,json=logType,proto3" json:"log_type,omitempty"` + OrgId string `protobuf:"bytes,4,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` } func (x *ListAvailableEventResultsRequest) Reset() { *x = ListAvailableEventResultsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[11] + mi := &file_artifacts_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -797,7 +1006,7 @@ func (x *ListAvailableEventResultsRequest) String() string { func (*ListAvailableEventResultsRequest) ProtoMessage() {} func (x *ListAvailableEventResultsRequest) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[11] + mi := &file_artifacts_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -810,7 +1019,7 @@ func (x *ListAvailableEventResultsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAvailableEventResultsRequest.ProtoReflect.Descriptor instead. func (*ListAvailableEventResultsRequest) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{11} + return file_artifacts_proto_rawDescGZIP(), []int{13} } func (x *ListAvailableEventResultsRequest) GetClientId() string { @@ -834,6 +1043,13 @@ func (x *ListAvailableEventResultsRequest) GetLogType() string { return "" } +func (x *ListAvailableEventResultsRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + type AvailableEvent struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -848,7 +1064,7 @@ type AvailableEvent struct { func (x *AvailableEvent) Reset() { *x = AvailableEvent{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[12] + mi := &file_artifacts_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -861,7 +1077,7 @@ func (x *AvailableEvent) String() string { func (*AvailableEvent) ProtoMessage() {} func (x *AvailableEvent) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[12] + mi := &file_artifacts_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -874,7 +1090,7 @@ func (x *AvailableEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use AvailableEvent.ProtoReflect.Descriptor instead. func (*AvailableEvent) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{12} + return file_artifacts_proto_rawDescGZIP(), []int{14} } func (x *AvailableEvent) GetArtifact() string { @@ -916,7 +1132,7 @@ type ListAvailableEventResultsResponse struct { func (x *ListAvailableEventResultsResponse) Reset() { *x = ListAvailableEventResultsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[13] + mi := &file_artifacts_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -929,7 +1145,7 @@ func (x *ListAvailableEventResultsResponse) String() string { func (*ListAvailableEventResultsResponse) ProtoMessage() {} func (x *ListAvailableEventResultsResponse) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[13] + mi := &file_artifacts_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -942,7 +1158,7 @@ func (x *ListAvailableEventResultsResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ListAvailableEventResultsResponse.ProtoReflect.Descriptor instead. func (*ListAvailableEventResultsResponse) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{13} + return file_artifacts_proto_rawDescGZIP(), []int{15} } func (x *ListAvailableEventResultsResponse) GetLogs() []*AvailableEvent { @@ -964,7 +1180,7 @@ type GetMonitoringStateRequest struct { func (x *GetMonitoringStateRequest) Reset() { *x = GetMonitoringStateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[14] + mi := &file_artifacts_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -977,7 +1193,7 @@ func (x *GetMonitoringStateRequest) String() string { func (*GetMonitoringStateRequest) ProtoMessage() {} func (x *GetMonitoringStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[14] + mi := &file_artifacts_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -990,7 +1206,7 @@ func (x *GetMonitoringStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMonitoringStateRequest.ProtoReflect.Descriptor instead. func (*GetMonitoringStateRequest) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{14} + return file_artifacts_proto_rawDescGZIP(), []int{16} } func (x *GetMonitoringStateRequest) GetLabel() string { @@ -1012,7 +1228,7 @@ type GetMonitoringStateResponse struct { func (x *GetMonitoringStateResponse) Reset() { *x = GetMonitoringStateResponse{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[15] + mi := &file_artifacts_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1025,7 +1241,7 @@ func (x *GetMonitoringStateResponse) String() string { func (*GetMonitoringStateResponse) ProtoMessage() {} func (x *GetMonitoringStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[15] + mi := &file_artifacts_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1038,7 +1254,7 @@ func (x *GetMonitoringStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMonitoringStateResponse.ProtoReflect.Descriptor instead. func (*GetMonitoringStateResponse) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{15} + return file_artifacts_proto_rawDescGZIP(), []int{17} } func (x *GetMonitoringStateResponse) GetRequests() []*SetMonitoringStateRequest { @@ -1062,7 +1278,7 @@ type SetMonitoringStateRequest struct { func (x *SetMonitoringStateRequest) Reset() { *x = SetMonitoringStateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_artifacts_proto_msgTypes[16] + mi := &file_artifacts_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1075,7 +1291,7 @@ func (x *SetMonitoringStateRequest) String() string { func (*SetMonitoringStateRequest) ProtoMessage() {} func (x *SetMonitoringStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_artifacts_proto_msgTypes[16] + mi := &file_artifacts_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1088,7 +1304,7 @@ func (x *SetMonitoringStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetMonitoringStateRequest.ProtoReflect.Descriptor instead. func (*SetMonitoringStateRequest) Descriptor() ([]byte, []int) { - return file_artifacts_proto_rawDescGZIP(), []int{16} + return file_artifacts_proto_rawDescGZIP(), []int{18} } func (x *SetMonitoringStateRequest) GetLabel() string { @@ -1115,168 +1331,200 @@ var file_artifacts_proto_rawDesc = []byte{ 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x23, 0x0a, 0x0d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xf3, 0x02, 0x0a, 0x13, 0x47, 0x65, - 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x36, 0x0a, 0x17, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x65, 0x76, 0x65, - 0x6e, 0x74, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x15, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x12, 0x38, 0x0a, 0x18, 0x69, 0x6e, 0x63, - 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x61, 0x72, 0x74, 0x69, - 0x66, 0x61, 0x63, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x69, 0x6e, 0x63, - 0x6c, 0x75, 0x64, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x74, 0x65, - 0x72, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, - 0x54, 0x65, 0x72, 0x6d, 0x12, 0x2a, 0x0a, 0x11, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x6f, - 0x66, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x0f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, - 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, - 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x06, 0x66, 0x69, 0x65, - 0x6c, 0x64, 0x73, 0x12, 0x3a, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, - 0x28, 0x09, 0x42, 0x24, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1e, 0x12, 0x1c, 0x41, 0x20, 0x6c, 0x69, - 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x20, - 0x74, 0x6f, 0x20, 0x66, 0x65, 0x74, 0x63, 0x68, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x22, - 0x44, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x1a, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x14, 0x12, 0x12, 0x54, 0x68, 0x65, - 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x2e, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x5b, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, - 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x08, - 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x28, - 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x22, 0x12, 0x20, 0x54, 0x68, 0x65, 0x20, 0x61, 0x72, 0x74, 0x69, - 0x66, 0x61, 0x63, 0x74, 0x20, 0x64, 0x61, 0x74, 0x61, 0x2c, 0x20, 0x6f, 0x72, 0x20, 0x61, 0x20, - 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x2e, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x22, 0xd8, 0x01, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x08, 0x61, 0x72, 0x74, - 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x28, 0xe2, 0xfc, 0xe3, - 0xc4, 0x01, 0x22, 0x12, 0x20, 0x54, 0x68, 0x65, 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, - 0x74, 0x20, 0x64, 0x61, 0x74, 0x61, 0x2c, 0x20, 0x6f, 0x72, 0x20, 0x61, 0x20, 0x64, 0x65, 0x66, - 0x61, 0x75, 0x6c, 0x74, 0x2e, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, - 0x5a, 0x0a, 0x02, 0x6f, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x42, 0x25, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1f, 0x12, 0x1d, 0x57, 0x68, 0x61, 0x74, 0x20, 0x74, - 0x6f, 0x20, 0x64, 0x6f, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x3f, 0x52, 0x02, 0x6f, 0x70, 0x22, 0x20, 0x0a, 0x09, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x45, 0x54, 0x10, - 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x01, 0x22, 0x45, 0x0a, - 0x11, 0x4c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x22, 0x7f, 0x0a, 0x18, 0x4c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x69, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x87, 0x01, 0x0a, 0x0d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, + 0x67, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x22, 0xf3, + 0x02, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x36, 0x0a, 0x17, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, + 0x65, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x12, 0x38, + 0x0a, 0x18, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x16, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, + 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x5f, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x54, 0x65, 0x72, 0x6d, 0x12, 0x2a, 0x0a, 0x11, 0x6e, 0x75, 0x6d, + 0x62, 0x65, 0x72, 0x5f, 0x6f, 0x66, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x06, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x3a, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x42, 0x24, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1e, 0x12, + 0x1c, 0x41, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x65, 0x74, 0x63, 0x68, 0x52, 0x05, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1a, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x14, + 0x12, 0x12, 0x54, 0x68, 0x65, 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x20, 0x6e, + 0x61, 0x6d, 0x65, 0x2e, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x5b, 0x0a, 0x13, 0x47, 0x65, + 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x44, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x28, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x22, 0x12, 0x20, 0x54, 0x68, 0x65, + 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x20, 0x64, 0x61, 0x74, 0x61, 0x2c, 0x20, + 0x6f, 0x72, 0x20, 0x61, 0x20, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x2e, 0x52, 0x08, 0x61, + 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x8a, 0x02, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x41, + 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, + 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x28, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x22, 0x12, 0x20, 0x54, 0x68, 0x65, 0x20, 0x61, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x20, 0x64, 0x61, 0x74, 0x61, 0x2c, 0x20, 0x6f, 0x72, 0x20, + 0x61, 0x20, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x2e, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, + 0x66, 0x61, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x5a, 0x0a, 0x02, 0x6f, 0x70, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x74, + 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x25, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, + 0x1f, 0x12, 0x1d, 0x57, 0x68, 0x61, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x64, 0x6f, 0x20, 0x77, 0x69, + 0x74, 0x68, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x3f, + 0x52, 0x02, 0x6f, 0x70, 0x22, 0x3e, 0x0a, 0x09, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x45, 0x54, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, + 0x4c, 0x45, 0x54, 0x45, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x10, + 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x41, 0x4e, 0x44, 0x5f, 0x53, + 0x45, 0x54, 0x10, 0x03, 0x22, 0x84, 0x01, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x41, 0x72, 0x74, 0x69, + 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x12, + 0x1a, 0x0a, 0x08, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x08, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x45, 0x0a, 0x11, 0x4c, + 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x22, 0xae, 0x01, 0x0a, 0x17, 0x4c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, + 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x06, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x76, 0x66, 0x73, 0x5f, 0x70, 0x61, + 0x74, 0x68, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x76, 0x66, 0x73, 0x50, 0x61, 0x74, + 0x68, 0x12, 0x20, 0x0a, 0x0c, 0x72, 0x65, 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x64, 0x6f, 0x5f, 0x69, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x72, 0x65, 0x61, 0x6c, 0x6c, 0x79, 0x44, + 0x6f, 0x49, 0x74, 0x22, 0x9a, 0x01, 0x0a, 0x18, 0x4c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x14, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, - 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x06, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x79, 0x0a, 0x0b, 0x41, 0x50, 0x49, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x42, 0x2f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x29, 0x12, 0x27, 0x41, 0x6e, 0x20, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x20, 0x6f, 0x63, 0x63, 0x75, 0x72, 0x72, 0x65, 0x64, 0x20, 0x73, - 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x2e, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x22, 0xf9, 0x03, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x28, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x22, 0x12, - 0x20, 0x54, 0x68, 0x65, 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x20, 0x66, 0x6f, - 0x72, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, 0x77, 0x65, 0x20, 0x72, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x4b, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x37, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, - 0x31, 0x12, 0x2f, 0x54, 0x68, 0x65, 0x20, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x74, 0x79, - 0x70, 0x65, 0x20, 0x77, 0x65, 0x20, 0x6e, 0x65, 0x65, 0x64, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, - 0x20, 0x4d, 0x4f, 0x4e, 0x49, 0x54, 0x4f, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x44, 0x41, 0x49, 0x4c, - 0x59, 0x29, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x30, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x6d, - 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x12, - 0x12, 0x10, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x65, 0x2e, 0x67, 0x2e, 0x20, 0x68, 0x74, - 0x6d, 0x6c, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x61, 0x79, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x61, 0x79, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, - 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, - 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, - 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x7c, - 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x42, 0x42, 0xe2, 0xfc, - 0xe3, 0xc4, 0x01, 0x3c, 0x12, 0x3a, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x70, 0x61, 0x72, - 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x73, 0x65, 0x20, 0x64, - 0x65, 0x70, 0x65, 0x6e, 0x64, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x79, 0x70, - 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x83, 0x01, 0x0a, - 0x11, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x09, 0x42, 0x22, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1c, 0x12, 0x1a, 0x45, 0x72, - 0x72, 0x6f, 0x72, 0x20, 0x6f, 0x72, 0x20, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x20, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x73, 0x22, 0x19, 0x0a, 0x17, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, - 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x69, 0x63, 0x74, 0x22, 0xe4, 0x01, - 0x0a, 0x20, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x88, 0x01, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x6b, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x65, 0x12, 0x63, - 0x54, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x49, 0x44, 0x20, 0x77, 0x65, - 0x20, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, - 0x62, 0x6c, 0x65, 0x20, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x6c, - 0x6f, 0x67, 0x73, 0x2e, 0x20, 0x49, 0x66, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x77, 0x65, - 0x20, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x27, 0x73, 0x20, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x6c, 0x6f, - 0x67, 0x73, 0x2e, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, - 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, - 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, - 0x54, 0x79, 0x70, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x0e, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, - 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x12, 0x2f, 0x0a, 0x0a, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x0a, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x6f, 0x77, 0x5f, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x05, 0x52, 0x0d, 0x72, 0x6f, - 0x77, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x6c, - 0x6f, 0x67, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x05, 0x52, 0x0d, 0x6c, 0x6f, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x73, 0x22, 0x4e, 0x0a, 0x21, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, - 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x76, - 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x04, 0x6c, 0x6f, - 0x67, 0x73, 0x22, 0x31, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, - 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4d, 0x6f, 0x6e, 0x69, - 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, - 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x73, 0x22, 0x69, 0x0a, 0x19, 0x53, 0x65, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, - 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, - 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, - 0x61, 0x62, 0x65, 0x6c, 0x12, 0x36, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, - 0x72, 0x67, 0x73, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x31, 0x5a, 0x2f, - 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, - 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x63, 0x74, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x76, 0x66, 0x73, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x76, 0x66, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x30, + 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, + 0x22, 0x79, 0x0a, 0x0b, 0x41, 0x50, 0x49, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x45, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x2f, + 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x29, 0x12, 0x27, 0x41, 0x6e, 0x20, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x20, 0x6f, 0x63, 0x63, 0x75, 0x72, 0x72, 0x65, 0x64, 0x20, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, + 0x67, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x52, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xf9, 0x03, 0x0a, 0x10, + 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x44, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x28, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x22, 0x12, 0x20, 0x54, 0x68, 0x65, 0x20, + 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x77, 0x68, 0x69, + 0x63, 0x68, 0x20, 0x77, 0x65, 0x20, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x08, 0x61, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x4b, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x37, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x31, 0x12, 0x2f, 0x54, 0x68, + 0x65, 0x20, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x77, 0x65, + 0x20, 0x6e, 0x65, 0x65, 0x64, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x20, 0x4d, 0x4f, 0x4e, 0x49, + 0x54, 0x4f, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x44, 0x41, 0x49, 0x4c, 0x59, 0x29, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x30, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x18, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x12, 0x12, 0x10, 0x46, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x20, 0x65, 0x2e, 0x67, 0x2e, 0x20, 0x68, 0x74, 0x6d, 0x6c, 0x52, 0x06, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, + 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, + 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, + 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, + 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, + 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x7c, 0x0a, 0x0a, 0x70, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x50, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x42, 0x42, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x3c, 0x12, + 0x3a, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x73, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x73, 0x65, 0x20, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, + 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x6f, 0x66, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x52, 0x0a, 0x70, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x83, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x52, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, + 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, + 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x42, + 0x22, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1c, 0x12, 0x1a, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x20, 0x6f, + 0x72, 0x20, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x20, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x73, 0x2e, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x19, 0x0a, + 0x17, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x69, 0x63, 0x74, 0x22, 0xfb, 0x01, 0x0a, 0x20, 0x4c, 0x69, 0x73, + 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x88, 0x01, + 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x6b, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x65, 0x12, 0x63, 0x54, 0x68, 0x65, 0x20, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x49, 0x44, 0x20, 0x77, 0x65, 0x20, 0x6c, 0x69, 0x73, 0x74, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x6d, + 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x20, + 0x49, 0x66, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x77, 0x65, 0x20, 0x6c, 0x69, 0x73, 0x74, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x27, 0x73, 0x20, 0x6d, 0x6f, + 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x52, 0x08, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, + 0x66, 0x61, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, + 0x66, 0x61, 0x63, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0xab, 0x01, 0x0a, 0x0e, 0x41, 0x76, 0x61, 0x69, 0x6c, + 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, 0x74, + 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x72, 0x74, + 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x2f, 0x0a, 0x0a, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x0a, 0x64, 0x65, 0x66, 0x69, + 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x6f, 0x77, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x05, 0x52, 0x0d, + 0x72, 0x6f, 0x77, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x12, 0x25, 0x0a, + 0x0e, 0x6c, 0x6f, 0x67, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x05, 0x52, 0x0d, 0x6c, 0x6f, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x73, 0x22, 0x4e, 0x0a, 0x21, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, + 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x6c, 0x6f, 0x67, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x04, + 0x6c, 0x6f, 0x67, 0x73, 0x22, 0x31, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, + 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4d, 0x6f, + 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x53, 0x65, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x73, 0x22, 0x69, 0x0a, 0x19, 0x53, 0x65, 0x74, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, + 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x36, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x41, 0x72, 0x67, 0x73, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x31, + 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, + 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1292,7 +1540,7 @@ func file_artifacts_proto_rawDescGZIP() []byte { } var file_artifacts_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_artifacts_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_artifacts_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_artifacts_proto_goTypes = []interface{}{ (SetArtifactRequest_Operation)(0), // 0: proto.SetArtifactRequest.Operation (*FieldSelector)(nil), // 1: proto.FieldSelector @@ -1300,31 +1548,33 @@ var file_artifacts_proto_goTypes = []interface{}{ (*GetArtifactRequest)(nil), // 3: proto.GetArtifactRequest (*GetArtifactResponse)(nil), // 4: proto.GetArtifactResponse (*SetArtifactRequest)(nil), // 5: proto.SetArtifactRequest - (*LoadArtifactError)(nil), // 6: proto.LoadArtifactError - (*LoadArtifactPackResponse)(nil), // 7: proto.LoadArtifactPackResponse - (*APIResponse)(nil), // 8: proto.APIResponse - (*GetReportRequest)(nil), // 9: proto.GetReportRequest - (*GetReportResponse)(nil), // 10: proto.GetReportResponse - (*ArtifactCompressionDict)(nil), // 11: proto.ArtifactCompressionDict - (*ListAvailableEventResultsRequest)(nil), // 12: proto.ListAvailableEventResultsRequest - (*AvailableEvent)(nil), // 13: proto.AvailableEvent - (*ListAvailableEventResultsResponse)(nil), // 14: proto.ListAvailableEventResultsResponse - (*GetMonitoringStateRequest)(nil), // 15: proto.GetMonitoringStateRequest - (*GetMonitoringStateResponse)(nil), // 16: proto.GetMonitoringStateResponse - (*SetMonitoringStateRequest)(nil), // 17: proto.SetMonitoringStateRequest - (*proto.ArtifactParameter)(nil), // 18: proto.ArtifactParameter - (*proto.Artifact)(nil), // 19: proto.Artifact - (*proto1.ArtifactCollectorArgs)(nil), // 20: proto.ArtifactCollectorArgs + (*SetArtifactResponse)(nil), // 6: proto.SetArtifactResponse + (*LoadArtifactError)(nil), // 7: proto.LoadArtifactError + (*LoadArtifactPackRequest)(nil), // 8: proto.LoadArtifactPackRequest + (*LoadArtifactPackResponse)(nil), // 9: proto.LoadArtifactPackResponse + (*APIResponse)(nil), // 10: proto.APIResponse + (*GetReportRequest)(nil), // 11: proto.GetReportRequest + (*GetReportResponse)(nil), // 12: proto.GetReportResponse + (*ArtifactCompressionDict)(nil), // 13: proto.ArtifactCompressionDict + (*ListAvailableEventResultsRequest)(nil), // 14: proto.ListAvailableEventResultsRequest + (*AvailableEvent)(nil), // 15: proto.AvailableEvent + (*ListAvailableEventResultsResponse)(nil), // 16: proto.ListAvailableEventResultsResponse + (*GetMonitoringStateRequest)(nil), // 17: proto.GetMonitoringStateRequest + (*GetMonitoringStateResponse)(nil), // 18: proto.GetMonitoringStateResponse + (*SetMonitoringStateRequest)(nil), // 19: proto.SetMonitoringStateRequest + (*proto.ArtifactParameter)(nil), // 20: proto.ArtifactParameter + (*proto.Artifact)(nil), // 21: proto.Artifact + (*proto1.ArtifactCollectorArgs)(nil), // 22: proto.ArtifactCollectorArgs } var file_artifacts_proto_depIdxs = []int32{ 1, // 0: proto.GetArtifactsRequest.fields:type_name -> proto.FieldSelector 0, // 1: proto.SetArtifactRequest.op:type_name -> proto.SetArtifactRequest.Operation - 6, // 2: proto.LoadArtifactPackResponse.errors:type_name -> proto.LoadArtifactError - 18, // 3: proto.GetReportRequest.parameters:type_name -> proto.ArtifactParameter - 19, // 4: proto.AvailableEvent.definition:type_name -> proto.Artifact - 13, // 5: proto.ListAvailableEventResultsResponse.logs:type_name -> proto.AvailableEvent - 17, // 6: proto.GetMonitoringStateResponse.requests:type_name -> proto.SetMonitoringStateRequest - 20, // 7: proto.SetMonitoringStateRequest.request:type_name -> proto.ArtifactCollectorArgs + 7, // 2: proto.LoadArtifactPackResponse.errors:type_name -> proto.LoadArtifactError + 20, // 3: proto.GetReportRequest.parameters:type_name -> proto.ArtifactParameter + 21, // 4: proto.AvailableEvent.definition:type_name -> proto.Artifact + 15, // 5: proto.ListAvailableEventResultsResponse.logs:type_name -> proto.AvailableEvent + 19, // 6: proto.GetMonitoringStateResponse.requests:type_name -> proto.SetMonitoringStateRequest + 22, // 7: proto.SetMonitoringStateRequest.request:type_name -> proto.ArtifactCollectorArgs 8, // [8:8] is the sub-list for method output_type 8, // [8:8] is the sub-list for method input_type 8, // [8:8] is the sub-list for extension type_name @@ -1399,7 +1649,7 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoadArtifactError); i { + switch v := v.(*SetArtifactResponse); i { case 0: return &v.state case 1: @@ -1411,7 +1661,7 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoadArtifactPackResponse); i { + switch v := v.(*LoadArtifactError); i { case 0: return &v.state case 1: @@ -1423,7 +1673,7 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*APIResponse); i { + switch v := v.(*LoadArtifactPackRequest); i { case 0: return &v.state case 1: @@ -1435,7 +1685,7 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetReportRequest); i { + switch v := v.(*LoadArtifactPackResponse); i { case 0: return &v.state case 1: @@ -1447,7 +1697,7 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetReportResponse); i { + switch v := v.(*APIResponse); i { case 0: return &v.state case 1: @@ -1459,7 +1709,7 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ArtifactCompressionDict); i { + switch v := v.(*GetReportRequest); i { case 0: return &v.state case 1: @@ -1471,7 +1721,7 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListAvailableEventResultsRequest); i { + switch v := v.(*GetReportResponse); i { case 0: return &v.state case 1: @@ -1483,7 +1733,7 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AvailableEvent); i { + switch v := v.(*ArtifactCompressionDict); i { case 0: return &v.state case 1: @@ -1495,7 +1745,7 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListAvailableEventResultsResponse); i { + switch v := v.(*ListAvailableEventResultsRequest); i { case 0: return &v.state case 1: @@ -1507,7 +1757,7 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetMonitoringStateRequest); i { + switch v := v.(*AvailableEvent); i { case 0: return &v.state case 1: @@ -1519,7 +1769,7 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetMonitoringStateResponse); i { + switch v := v.(*ListAvailableEventResultsResponse); i { case 0: return &v.state case 1: @@ -1531,6 +1781,30 @@ func file_artifacts_proto_init() { } } file_artifacts_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetMonitoringStateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifacts_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetMonitoringStateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifacts_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SetMonitoringStateRequest); i { case 0: return &v.state @@ -1549,7 +1823,7 @@ func file_artifacts_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_artifacts_proto_rawDesc, NumEnums: 1, - NumMessages: 17, + NumMessages: 19, NumExtensions: 0, NumServices: 0, }, diff --git a/api/proto/artifacts.proto b/api/proto/artifacts.proto index eeaf8ce5e..190cbe208 100644 --- a/api/proto/artifacts.proto +++ b/api/proto/artifacts.proto @@ -10,6 +10,10 @@ option go_package = "www.velocidex.com/golang/velociraptor/api/proto"; message FieldSelector { bool name = 1; + bool description = 2; + bool type = 3; + bool sources = 4; + bool tags = 5; } message GetArtifactsRequest { @@ -32,10 +36,6 @@ message GetArtifactsRequest { } message GetArtifactRequest { - // Deprecated. - // string vfs_path = 1 [(sem_type) = { - // description: "The vfs path relative to the artifacts definition store." - // }]; string name = 2 [(sem_type) = { description: "The artifact name." }]; @@ -49,18 +49,20 @@ message GetArtifactResponse { message SetArtifactRequest { - // Deprecated - // string vfs_path = 1 [(sem_type) = { - // description: "The vfs path relative to the artifacts definition store." - // }]; - string artifact = 2 [(sem_type) = { description: "The artifact data, or a default.", }]; + // Also set these tags. + repeated string tags = 4; + enum Operation { SET = 0; DELETE = 1; + CHECK = 2; + + // Only set the artifact if there are no errors or warnings. + CHECK_AND_SET = 3; } Operation op = 3 [(sem_type) = { @@ -68,14 +70,42 @@ message SetArtifactRequest { }]; } +message SetArtifactResponse { + bool error = 1; + string error_message = 2; + + repeated string errors = 3; + repeated string warnings = 4; +} + message LoadArtifactError { string filename = 1; string error = 2; } +message LoadArtifactPackRequest { + string prefix = 1; + repeated string tags = 6; + string filter = 2; + + // The API can specify the archive two ways: + + // 1. The raw data is attached in the data field. The server will + // store the data locally and return its VFS path components. + // 2. The caller can specify these components in subsequent calls + // to operate on the already uploaded file. + + // NOTE: the vfs path must be in the VFS temp directory. + bytes data = 3; + repeated string vfs_path = 5; + + bool really_do_it = 4; +} + + message LoadArtifactPackResponse { repeated string successful_artifacts = 1; - + repeated string vfs_path = 3; repeated LoadArtifactError errors = 2; } @@ -147,6 +177,8 @@ message ListAvailableEventResultsRequest { // This can be empty or "logs" to list the logs. string log_type = 3; + + string org_id = 4; } message AvailableEvent { @@ -176,4 +208,4 @@ message SetMonitoringStateRequest { string label = 1; ArtifactCollectorArgs request = 2; -} \ No newline at end of file +} diff --git a/api/proto/clients.pb.go b/api/proto/clients.pb.go index f3eb9dd9c..9074686ff 100644 --- a/api/proto/clients.pb.go +++ b/api/proto/clients.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: clients.proto package proto @@ -21,58 +18,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type ApiClient_IPAddressClass int32 - -const ( - ApiClient_UNKNOWN ApiClient_IPAddressClass = 0 - ApiClient_INTERNAL ApiClient_IPAddressClass = 1 - ApiClient_EXTERNAL ApiClient_IPAddressClass = 2 - ApiClient_VPN ApiClient_IPAddressClass = 3 -) - -// Enum value maps for ApiClient_IPAddressClass. -var ( - ApiClient_IPAddressClass_name = map[int32]string{ - 0: "UNKNOWN", - 1: "INTERNAL", - 2: "EXTERNAL", - 3: "VPN", - } - ApiClient_IPAddressClass_value = map[string]int32{ - "UNKNOWN": 0, - "INTERNAL": 1, - "EXTERNAL": 2, - "VPN": 3, - } -) - -func (x ApiClient_IPAddressClass) Enum() *ApiClient_IPAddressClass { - p := new(ApiClient_IPAddressClass) - *p = x - return p -} - -func (x ApiClient_IPAddressClass) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ApiClient_IPAddressClass) Descriptor() protoreflect.EnumDescriptor { - return file_clients_proto_enumTypes[0].Descriptor() -} - -func (ApiClient_IPAddressClass) Type() protoreflect.EnumType { - return &file_clients_proto_enumTypes[0] -} - -func (x ApiClient_IPAddressClass) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ApiClient_IPAddressClass.Descriptor instead. -func (ApiClient_IPAddressClass) EnumDescriptor() ([]byte, []int) { - return file_clients_proto_rawDescGZIP(), []int{1, 0} -} - type SearchClientsRequest_SortingSense int32 const ( @@ -106,11 +51,11 @@ func (x SearchClientsRequest_SortingSense) String() string { } func (SearchClientsRequest_SortingSense) Descriptor() protoreflect.EnumDescriptor { - return file_clients_proto_enumTypes[1].Descriptor() + return file_clients_proto_enumTypes[0].Descriptor() } func (SearchClientsRequest_SortingSense) Type() protoreflect.EnumType { - return &file_clients_proto_enumTypes[1] + return &file_clients_proto_enumTypes[0] } func (x SearchClientsRequest_SortingSense) Number() protoreflect.EnumNumber { @@ -154,11 +99,11 @@ func (x SearchClientsRequest_Filters) String() string { } func (SearchClientsRequest_Filters) Descriptor() protoreflect.EnumDescriptor { - return file_clients_proto_enumTypes[2].Descriptor() + return file_clients_proto_enumTypes[1].Descriptor() } func (SearchClientsRequest_Filters) Type() protoreflect.EnumType { - return &file_clients_proto_enumTypes[2] + return &file_clients_proto_enumTypes[1] } func (x SearchClientsRequest_Filters) Number() protoreflect.EnumNumber { @@ -181,6 +126,7 @@ type AgentInformation struct { Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` BuildTime string `protobuf:"bytes,3,opt,name=build_time,json=buildTime,proto3" json:"build_time,omitempty"` + BuildUrl string `protobuf:"bytes,4,opt,name=build_url,json=buildUrl,proto3" json:"build_url,omitempty"` } func (x *AgentInformation) Reset() { @@ -236,26 +182,35 @@ func (x *AgentInformation) GetBuildTime() string { return "" } -// Describe a client. We fill in some metadata about the client but -// this is by no means exhaustive. +func (x *AgentInformation) GetBuildUrl() string { + if x != nil { + return x.BuildUrl + } + return "" +} + +// TODO: This is an older protobuf that is now largely supeceeded by +// actions_proto.ClientInfo. We need to replace use of this protobuf +// by ClientInfo. type ApiClient struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` - AgentInformation *AgentInformation `protobuf:"bytes,2,opt,name=agent_information,json=agentInformation,proto3" json:"agent_information,omitempty"` - OsInfo *Uname `protobuf:"bytes,3,opt,name=os_info,json=osInfo,proto3" json:"os_info,omitempty"` - FirstSeenAt uint64 `protobuf:"varint,6,opt,name=first_seen_at,json=firstSeenAt,proto3" json:"first_seen_at,omitempty"` - LastSeenAt uint64 `protobuf:"varint,7,opt,name=last_seen_at,json=lastSeenAt,proto3" json:"last_seen_at,omitempty"` - LastBootedAt uint64 `protobuf:"varint,8,opt,name=last_booted_at,json=lastBootedAt,proto3" json:"last_booted_at,omitempty"` - LastClock uint64 `protobuf:"varint,9,opt,name=last_clock,json=lastClock,proto3" json:"last_clock,omitempty"` - LastCrashAt uint64 `protobuf:"varint,10,opt,name=last_crash_at,json=lastCrashAt,proto3" json:"last_crash_at,omitempty"` - LastIp string `protobuf:"bytes,16,opt,name=last_ip,json=lastIp,proto3" json:"last_ip,omitempty"` - LastInterrogateFlowId string `protobuf:"bytes,19,opt,name=last_interrogate_flow_id,json=lastInterrogateFlowId,proto3" json:"last_interrogate_flow_id,omitempty"` - LastInterrogateArtifactName string `protobuf:"bytes,21,opt,name=last_interrogate_artifact_name,json=lastInterrogateArtifactName,proto3" json:"last_interrogate_artifact_name,omitempty"` - LastIpClass ApiClient_IPAddressClass `protobuf:"varint,17,opt,name=last_ip_class,json=lastIpClass,proto3,enum=proto.ApiClient_IPAddressClass" json:"last_ip_class,omitempty"` - Labels []string `protobuf:"bytes,18,rep,name=labels,proto3" json:"labels,omitempty"` + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + AgentInformation *AgentInformation `protobuf:"bytes,2,opt,name=agent_information,json=agentInformation,proto3" json:"agent_information,omitempty"` + OsInfo *Uname `protobuf:"bytes,3,opt,name=os_info,json=osInfo,proto3" json:"os_info,omitempty"` + FirstSeenAt uint64 `protobuf:"varint,6,opt,name=first_seen_at,json=firstSeenAt,proto3" json:"first_seen_at,omitempty"` + LastSeenAt uint64 `protobuf:"varint,7,opt,name=last_seen_at,json=lastSeenAt,proto3" json:"last_seen_at,omitempty"` + LastIp string `protobuf:"bytes,16,opt,name=last_ip,json=lastIp,proto3" json:"last_ip,omitempty"` + LastInterrogateFlowId string `protobuf:"bytes,19,opt,name=last_interrogate_flow_id,json=lastInterrogateFlowId,proto3" json:"last_interrogate_flow_id,omitempty"` + LastInterrogateArtifactName string `protobuf:"bytes,21,opt,name=last_interrogate_artifact_name,json=lastInterrogateArtifactName,proto3" json:"last_interrogate_artifact_name,omitempty"` + Labels []string `protobuf:"bytes,18,rep,name=labels,proto3" json:"labels,omitempty"` + LastHuntTimestamp uint64 `protobuf:"varint,22,opt,name=last_hunt_timestamp,json=lastHuntTimestamp,proto3" json:"last_hunt_timestamp,omitempty"` + LastEventTableVersion uint64 `protobuf:"varint,23,opt,name=last_event_table_version,json=lastEventTableVersion,proto3" json:"last_event_table_version,omitempty"` + // Last time the labels on this client were updated. + LastLabelTimestamp uint64 `protobuf:"varint,24,opt,name=last_label_timestamp,json=lastLabelTimestamp,proto3" json:"last_label_timestamp,omitempty"` + InFlightFlows map[string]int64 `protobuf:"bytes,25,rep,name=in_flight_flows,json=inFlightFlows,proto3" json:"in_flight_flows,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` } func (x *ApiClient) Reset() { @@ -325,58 +280,58 @@ func (x *ApiClient) GetLastSeenAt() uint64 { return 0 } -func (x *ApiClient) GetLastBootedAt() uint64 { +func (x *ApiClient) GetLastIp() string { if x != nil { - return x.LastBootedAt + return x.LastIp } - return 0 + return "" } -func (x *ApiClient) GetLastClock() uint64 { +func (x *ApiClient) GetLastInterrogateFlowId() string { if x != nil { - return x.LastClock + return x.LastInterrogateFlowId } - return 0 + return "" } -func (x *ApiClient) GetLastCrashAt() uint64 { +func (x *ApiClient) GetLastInterrogateArtifactName() string { if x != nil { - return x.LastCrashAt + return x.LastInterrogateArtifactName } - return 0 + return "" } -func (x *ApiClient) GetLastIp() string { +func (x *ApiClient) GetLabels() []string { if x != nil { - return x.LastIp + return x.Labels } - return "" + return nil } -func (x *ApiClient) GetLastInterrogateFlowId() string { +func (x *ApiClient) GetLastHuntTimestamp() uint64 { if x != nil { - return x.LastInterrogateFlowId + return x.LastHuntTimestamp } - return "" + return 0 } -func (x *ApiClient) GetLastInterrogateArtifactName() string { +func (x *ApiClient) GetLastEventTableVersion() uint64 { if x != nil { - return x.LastInterrogateArtifactName + return x.LastEventTableVersion } - return "" + return 0 } -func (x *ApiClient) GetLastIpClass() ApiClient_IPAddressClass { +func (x *ApiClient) GetLastLabelTimestamp() uint64 { if x != nil { - return x.LastIpClass + return x.LastLabelTimestamp } - return ApiClient_UNKNOWN + return 0 } -func (x *ApiClient) GetLabels() []string { +func (x *ApiClient) GetInFlightFlows() map[string]int64 { if x != nil { - return x.Labels + return x.InFlightFlows } return nil } @@ -476,8 +431,10 @@ type SearchClientsResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Items []*ApiClient `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` - Names []string `protobuf:"bytes,2,rep,name=names,proto3" json:"names,omitempty"` + Items []*ApiClient `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + Names []string `protobuf:"bytes,2,rep,name=names,proto3" json:"names,omitempty"` + Total uint64 `protobuf:"varint,3,opt,name=total,proto3" json:"total,omitempty"` + SearchTerm *SearchClientsRequest `protobuf:"bytes,4,opt,name=search_term,json=searchTerm,proto3" json:"search_term,omitempty"` } func (x *SearchClientsResponse) Reset() { @@ -526,6 +483,20 @@ func (x *SearchClientsResponse) GetNames() []string { return nil } +func (x *SearchClientsResponse) GetTotal() uint64 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *SearchClientsResponse) GetSearchTerm() *SearchClientsRequest { + if x != nil { + return x.SearchTerm + } + return nil +} + type GetClientRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -820,28 +791,88 @@ func (x *ClientMetadata) GetClientId() string { return "" } +type SetClientMetadataRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Add []*ClientMetadataItem `protobuf:"bytes,1,rep,name=add,proto3" json:"add,omitempty"` + // A list of keys to remove + Remove []string `protobuf:"bytes,2,rep,name=remove,proto3" json:"remove,omitempty"` + ClientId string `protobuf:"bytes,3,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` +} + +func (x *SetClientMetadataRequest) Reset() { + *x = SetClientMetadataRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_clients_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetClientMetadataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetClientMetadataRequest) ProtoMessage() {} + +func (x *SetClientMetadataRequest) ProtoReflect() protoreflect.Message { + mi := &file_clients_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetClientMetadataRequest.ProtoReflect.Descriptor instead. +func (*SetClientMetadataRequest) Descriptor() ([]byte, []int) { + return file_clients_proto_rawDescGZIP(), []int{9} +} + +func (x *SetClientMetadataRequest) GetAdd() []*ClientMetadataItem { + if x != nil { + return x.Add + } + return nil +} + +func (x *SetClientMetadataRequest) GetRemove() []string { + if x != nil { + return x.Remove + } + return nil +} + +func (x *SetClientMetadataRequest) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + // Message to carry uname information. type Uname struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - System string `protobuf:"bytes,1,opt,name=system,proto3" json:"system,omitempty"` - Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` - Release string `protobuf:"bytes,3,opt,name=release,proto3" json:"release,omitempty"` - Version string `protobuf:"bytes,4,opt,name=version,proto3" json:"version,omitempty"` - Machine string `protobuf:"bytes,5,opt,name=machine,proto3" json:"machine,omitempty"` - Kernel string `protobuf:"bytes,6,opt,name=kernel,proto3" json:"kernel,omitempty"` - Fqdn string `protobuf:"bytes,7,opt,name=fqdn,proto3" json:"fqdn,omitempty"` - InstallDate uint64 `protobuf:"varint,8,opt,name=install_date,json=installDate,proto3" json:"install_date,omitempty"` - LibcVer string `protobuf:"bytes,9,opt,name=libc_ver,json=libcVer,proto3" json:"libc_ver,omitempty"` - Architecture string `protobuf:"bytes,10,opt,name=architecture,proto3" json:"architecture,omitempty"` + System string `protobuf:"bytes,1,opt,name=system,proto3" json:"system,omitempty"` + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + Release string `protobuf:"bytes,3,opt,name=release,proto3" json:"release,omitempty"` + Machine string `protobuf:"bytes,5,opt,name=machine,proto3" json:"machine,omitempty"` + Fqdn string `protobuf:"bytes,7,opt,name=fqdn,proto3" json:"fqdn,omitempty"` + MacAddresses []string `protobuf:"bytes,11,rep,name=mac_addresses,json=macAddresses,proto3" json:"mac_addresses,omitempty"` } func (x *Uname) Reset() { *x = Uname{} if protoimpl.UnsafeEnabled { - mi := &file_clients_proto_msgTypes[9] + mi := &file_clients_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -854,7 +885,7 @@ func (x *Uname) String() string { func (*Uname) ProtoMessage() {} func (x *Uname) ProtoReflect() protoreflect.Message { - mi := &file_clients_proto_msgTypes[9] + mi := &file_clients_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -867,7 +898,7 @@ func (x *Uname) ProtoReflect() protoreflect.Message { // Deprecated: Use Uname.ProtoReflect.Descriptor instead. func (*Uname) Descriptor() ([]byte, []int) { - return file_clients_proto_rawDescGZIP(), []int{9} + return file_clients_proto_rawDescGZIP(), []int{10} } func (x *Uname) GetSystem() string { @@ -891,13 +922,6 @@ func (x *Uname) GetRelease() string { return "" } -func (x *Uname) GetVersion() string { - if x != nil { - return x.Version - } - return "" -} - func (x *Uname) GetMachine() string { if x != nil { return x.Machine @@ -905,13 +929,6 @@ func (x *Uname) GetMachine() string { return "" } -func (x *Uname) GetKernel() string { - if x != nil { - return x.Kernel - } - return "" -} - func (x *Uname) GetFqdn() string { if x != nil { return x.Fqdn @@ -919,25 +936,11 @@ func (x *Uname) GetFqdn() string { return "" } -func (x *Uname) GetInstallDate() uint64 { - if x != nil { - return x.InstallDate - } - return 0 -} - -func (x *Uname) GetLibcVer() string { - if x != nil { - return x.LibcVer - } - return "" -} - -func (x *Uname) GetArchitecture() string { +func (x *Uname) GetMacAddresses() []string { if x != nil { - return x.Architecture + return x.MacAddresses } - return "" + return nil } type IndexRecord struct { @@ -954,7 +957,7 @@ type IndexRecord struct { func (x *IndexRecord) Reset() { *x = IndexRecord{} if protoimpl.UnsafeEnabled { - mi := &file_clients_proto_msgTypes[10] + mi := &file_clients_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -967,7 +970,7 @@ func (x *IndexRecord) String() string { func (*IndexRecord) ProtoMessage() {} func (x *IndexRecord) ProtoReflect() protoreflect.Message { - mi := &file_clients_proto_msgTypes[10] + mi := &file_clients_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -980,7 +983,7 @@ func (x *IndexRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use IndexRecord.ProtoReflect.Descriptor instead. func (*IndexRecord) Descriptor() ([]byte, []int) { - return file_clients_proto_rawDescGZIP(), []int{10} + return file_clients_proto_rawDescGZIP(), []int{11} } func (x *IndexRecord) GetEntity() string { @@ -1002,202 +1005,182 @@ var File_clients_proto protoreflect.FileDescriptor var file_clients_proto_rawDesc = []byte{ 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, - 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5f, 0x0a, 0x10, + 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7c, 0x0a, 0x10, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xe2, 0x07, - 0x0a, 0x09, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x3f, 0x0a, 0x09, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x22, - 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1c, 0x0a, 0x0b, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x49, 0x64, 0x12, 0x0d, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, - 0x69, 0x64, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x44, 0x0a, 0x11, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x10, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x07, 0x6f, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x6e, 0x61, 0x6d, - 0x65, 0x52, 0x06, 0x6f, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x74, 0x0a, 0x0d, 0x66, 0x69, 0x72, - 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, - 0x42, 0x50, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x4a, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, - 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x3b, 0x57, 0x68, 0x65, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x77, 0x61, 0x73, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, - 0x20, 0x73, 0x65, 0x65, 0x6e, 0x20, 0x28, 0x69, 0x2e, 0x65, 0x2e, 0x20, 0x77, 0x68, 0x65, 0x6e, - 0x20, 0x69, 0x74, 0x20, 0x77, 0x61, 0x73, 0x20, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x64, - 0x29, 0x2e, 0x52, 0x0b, 0x66, 0x69, 0x72, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, - 0x58, 0x0a, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x36, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x30, 0x0a, 0x0b, 0x52, - 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x21, 0x4c, 0x61, 0x73, 0x74, - 0x20, 0x74, 0x69, 0x6d, 0x65, 0x20, 0x77, 0x68, 0x65, 0x6e, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x20, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x2e, 0x52, 0x0a, 0x6c, - 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x4a, 0x0a, 0x0e, 0x6c, 0x61, 0x73, - 0x74, 0x5f, 0x62, 0x6f, 0x6f, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x04, 0x42, 0x24, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1e, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, - 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x0f, 0x4c, 0x61, 0x73, 0x74, 0x20, 0x62, 0x6f, 0x6f, - 0x74, 0x20, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x52, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x42, 0x6f, 0x6f, - 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x5e, 0x0a, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x63, 0x6c, - 0x6f, 0x63, 0x6b, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x42, 0x3f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, - 0x39, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x2a, - 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x63, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x20, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x20, 0x64, 0x75, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x6c, 0x61, 0x74, 0x65, 0x73, - 0x74, 0x20, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x2e, 0x52, 0x09, 0x6c, 0x61, 0x73, 0x74, - 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x49, 0x0a, 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x63, 0x72, - 0x61, 0x73, 0x68, 0x5f, 0x61, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x42, 0x25, 0xe2, 0xfc, - 0xe3, 0xc4, 0x01, 0x1f, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, - 0x65, 0x12, 0x10, 0x4c, 0x61, 0x73, 0x74, 0x20, 0x63, 0x72, 0x61, 0x73, 0x68, 0x20, 0x74, 0x69, - 0x6d, 0x65, 0x2e, 0x52, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x43, 0x72, 0x61, 0x73, 0x68, 0x41, 0x74, - 0x12, 0x41, 0x0a, 0x07, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x10, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x28, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x22, 0x12, 0x20, 0x54, 0x68, 0x65, 0x20, 0x6c, - 0x61, 0x73, 0x74, 0x20, 0x73, 0x65, 0x65, 0x6e, 0x20, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x20, - 0x41, 0x50, 0x49, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x06, 0x6c, 0x61, 0x73, - 0x74, 0x49, 0x70, 0x12, 0x37, 0x0a, 0x18, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x5f, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, - 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x43, 0x0a, 0x1e, - 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, - 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x15, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x72, - 0x6f, 0x67, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x43, 0x0a, 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x70, 0x5f, 0x63, 0x6c, 0x61, - 0x73, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x50, 0x41, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x52, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x49, - 0x70, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, - 0x18, 0x12, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x22, 0x42, - 0x0a, 0x0e, 0x49, 0x50, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x43, 0x6c, 0x61, 0x73, 0x73, - 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, - 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x45, - 0x58, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x56, 0x50, 0x4e, - 0x10, 0x03, 0x22, 0xd3, 0x02, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, - 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, - 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, - 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, - 0x1b, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x08, 0x6e, 0x61, 0x6d, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x3c, 0x0a, 0x04, - 0x73, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x53, - 0x65, 0x6e, 0x73, 0x65, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x12, 0x3b, 0x0a, 0x06, 0x66, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x52, - 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x38, 0x0a, 0x0c, 0x53, 0x6f, 0x72, 0x74, 0x69, - 0x6e, 0x67, 0x53, 0x65, 0x6e, 0x73, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x55, 0x4e, 0x53, 0x4f, 0x52, - 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x55, 0x50, - 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x10, - 0x02, 0x22, 0x25, 0x0a, 0x07, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x12, 0x0e, 0x0a, 0x0a, - 0x55, 0x4e, 0x46, 0x49, 0x4c, 0x54, 0x45, 0x52, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, - 0x4f, 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x01, 0x22, 0xa6, 0x01, 0x0a, 0x15, 0x53, 0x65, 0x61, - 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x65, 0x0a, 0x05, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, 0x4f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, - 0x49, 0x12, 0x47, 0x49, 0x66, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x20, - 0x69, 0x73, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x77, 0x65, 0x20, 0x6f, - 0x6e, 0x6c, 0x79, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x20, 0x68, 0x65, 0x72, 0x65, 0x2e, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x22, 0x9f, 0x01, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x49, 0x64, 0x12, 0x4f, 0x0a, 0x0b, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x77, 0x65, 0x69, 0x67, - 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x42, 0x2d, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x27, - 0x12, 0x25, 0x49, 0x66, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x63, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x20, 0x73, 0x6f, 0x6d, 0x65, 0x20, 0x69, 0x6e, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x0b, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x77, 0x65, - 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, - 0x72, 0x75, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x4d, 0x72, 0x75, 0x22, 0x6a, 0x0a, 0x13, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x43, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, - 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x61, 0x62, - 0x65, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, - 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, - 0x42, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, - 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x14, 0x0a, - 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, - 0x62, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x22, 0x5e, 0x0a, 0x0e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x2f, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, - 0x74, 0x65, 0x6d, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, - 0x64, 0x22, 0x85, 0x07, 0x0a, 0x05, 0x55, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x06, 0x73, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x33, 0xe2, 0xfc, 0xe3, - 0xc4, 0x01, 0x2d, 0x12, 0x2b, 0x54, 0x68, 0x65, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, - 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x28, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, - 0x73, 0x7c, 0x44, 0x61, 0x72, 0x77, 0x69, 0x6e, 0x7c, 0x4c, 0x69, 0x6e, 0x75, 0x78, 0x29, 0x2e, - 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x40, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x24, 0xe2, 0xfc, 0xe3, 0xc4, - 0x01, 0x1e, 0x12, 0x1c, 0x54, 0x68, 0x65, 0x20, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, - 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x2e, - 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x50, 0x0a, 0x07, 0x72, 0x65, - 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x36, 0xe2, 0xfc, 0xe3, - 0xc4, 0x01, 0x30, 0x12, 0x2e, 0x54, 0x68, 0x65, 0x20, 0x4f, 0x53, 0x20, 0x72, 0x65, 0x6c, 0x65, - 0x61, 0x73, 0x65, 0x20, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x20, 0x65, - 0x2e, 0x67, 0x2e, 0x20, 0x37, 0x2c, 0x20, 0x4f, 0x53, 0x58, 0x2c, 0x20, 0x64, 0x65, 0x62, 0x69, - 0x61, 0x6e, 0x2e, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x3a, 0xe2, - 0xfc, 0xe3, 0xc4, 0x01, 0x34, 0x12, 0x32, 0x54, 0x68, 0x65, 0x20, 0x4f, 0x53, 0x20, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x49, 0x44, 0x20, 0x65, 0x2e, 0x67, 0x2e, 0x20, 0x36, 0x2e, - 0x31, 0x2e, 0x37, 0x36, 0x30, 0x31, 0x53, 0x50, 0x31, 0x2c, 0x20, 0x31, 0x30, 0x2e, 0x39, 0x2e, - 0x32, 0x2c, 0x20, 0x31, 0x34, 0x2e, 0x30, 0x34, 0x2e, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x33, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x2d, 0x12, 0x2b, 0x54, 0x68, 0x65, - 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, - 0x74, 0x75, 0x72, 0x65, 0x20, 0x65, 0x2e, 0x67, 0x2e, 0x20, 0x41, 0x4d, 0x44, 0x36, 0x34, 0x2c, - 0x20, 0x78, 0x38, 0x36, 0x5f, 0x36, 0x34, 0x2e, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, - 0x65, 0x12, 0x5a, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x42, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x3c, 0x12, 0x3a, 0x54, 0x68, 0x65, 0x20, 0x6b, - 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x73, 0x74, - 0x72, 0x69, 0x6e, 0x67, 0x20, 0x65, 0x2e, 0x67, 0x2e, 0x20, 0x36, 0x2e, 0x31, 0x2e, 0x37, 0x36, - 0x30, 0x31, 0x2c, 0x20, 0x31, 0x33, 0x2e, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x33, 0x2e, 0x31, 0x35, - 0x2d, 0x72, 0x63, 0x32, 0x2e, 0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x45, 0x0a, - 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, 0x31, 0xe2, 0xfc, 0xe3, - 0xc4, 0x01, 0x2b, 0x12, 0x29, 0x54, 0x68, 0x65, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x27, - 0x73, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x79, 0x20, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, - 0x64, 0x20, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x2e, 0x52, 0x04, - 0x66, 0x71, 0x64, 0x6e, 0x12, 0x52, 0x0a, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x5f, - 0x64, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x42, 0x2f, 0xe2, 0xfc, 0xe3, 0xc4, - 0x01, 0x29, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, - 0x1a, 0x57, 0x68, 0x65, 0x6e, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, 0x77, 0x61, 0x73, - 0x20, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x2e, 0x52, 0x0b, 0x69, 0x6e, 0x73, - 0x74, 0x61, 0x6c, 0x6c, 0x44, 0x61, 0x74, 0x65, 0x12, 0x38, 0x0a, 0x08, 0x6c, 0x69, 0x62, 0x63, - 0x5f, 0x76, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1d, 0xe2, 0xfc, 0xe3, 0xc4, - 0x01, 0x17, 0x12, 0x15, 0x54, 0x68, 0x65, 0x20, 0x43, 0x20, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, - 0x79, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x69, 0x62, 0x63, 0x56, - 0x65, 0x72, 0x12, 0xc4, 0x01, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, - 0x75, 0x72, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x9f, 0x01, 0xe2, 0xfc, 0xe3, 0xc4, - 0x01, 0x98, 0x01, 0x12, 0x95, 0x01, 0x54, 0x68, 0x65, 0x20, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, - 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x62, - 0x69, 0x6e, 0x61, 0x72, 0x79, 0x2e, 0x20, 0x28, 0x4e, 0x6f, 0x74, 0x65, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x62, 0x65, 0x20, 0x64, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, - 0x6e, 0x74, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6d, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x65, 0x20, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, - 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x61, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, - 0x61, 0x20, 0x33, 0x32, 0x20, 0x62, 0x69, 0x74, 0x20, 0x62, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x20, - 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x20, 0x6f, 0x6e, 0x20, 0x61, 0x20, 0x36, 0x34, 0x20, - 0x62, 0x69, 0x74, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x29, 0x52, 0x0c, 0x61, 0x72, 0x63, - 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x22, 0x39, 0x0a, 0x0b, 0x49, 0x6e, 0x64, - 0x65, 0x78, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x74, 0x65, 0x72, 0x6d, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, - 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, - 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, + 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x55, 0x72, 0x6c, 0x22, 0x8c, 0x07, 0x0a, 0x09, 0x41, + 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x3f, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x22, 0xe2, 0xfc, 0xe3, + 0xc4, 0x01, 0x1c, 0x0a, 0x0b, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, + 0x12, 0x0d, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x64, 0x52, + 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x44, 0x0a, 0x11, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x67, 0x65, + 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x10, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x25, 0x0a, 0x07, 0x6f, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x06, + 0x6f, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x74, 0x0a, 0x0d, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, + 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x42, 0x50, 0xe2, + 0xfc, 0xe3, 0xc4, 0x01, 0x4a, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, 0x69, + 0x6d, 0x65, 0x12, 0x3b, 0x57, 0x68, 0x65, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x20, 0x77, 0x61, 0x73, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x73, 0x65, + 0x65, 0x6e, 0x20, 0x28, 0x69, 0x2e, 0x65, 0x2e, 0x20, 0x77, 0x68, 0x65, 0x6e, 0x20, 0x69, 0x74, + 0x20, 0x77, 0x61, 0x73, 0x20, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x64, 0x29, 0x2e, 0x52, + 0x0b, 0x66, 0x69, 0x72, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x58, 0x0a, 0x0c, + 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x04, 0x42, 0x36, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x30, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, + 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x21, 0x4c, 0x61, 0x73, 0x74, 0x20, 0x74, 0x69, + 0x6d, 0x65, 0x20, 0x77, 0x68, 0x65, 0x6e, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x2e, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, + 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x41, 0x0a, 0x07, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, + 0x70, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x42, 0x28, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x22, 0x12, + 0x20, 0x54, 0x68, 0x65, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x20, 0x73, 0x65, 0x65, 0x6e, 0x20, 0x72, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x20, 0x41, 0x50, 0x49, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x52, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x70, 0x12, 0x37, 0x0a, 0x18, 0x6c, 0x61, 0x73, + 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x5f, 0x66, 0x6c, + 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x61, 0x73, + 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x6f, 0x77, + 0x49, 0x64, 0x12, 0x43, 0x0a, 0x1e, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x6c, 0x61, 0x73, 0x74, + 0x49, 0x6e, 0x74, 0x65, 0x72, 0x72, 0x6f, 0x67, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, + 0x73, 0x18, 0x12, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, + 0x2e, 0x0a, 0x13, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x16, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x6c, 0x61, + 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, + 0x37, 0x0a, 0x18, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x17, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x15, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, + 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, + 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x18, 0x18, 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x4c, 0x61, 0x62, 0x65, + 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x4b, 0x0a, 0x0f, 0x69, 0x6e, + 0x5f, 0x66, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x5f, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x18, 0x19, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6e, 0x46, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x6c, + 0x6f, 0x77, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x69, 0x6e, 0x46, 0x6c, 0x69, 0x67, + 0x68, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x73, 0x1a, 0x40, 0x0a, 0x12, 0x49, 0x6e, 0x46, 0x6c, 0x69, + 0x67, 0x68, 0x74, 0x46, 0x6c, 0x6f, 0x77, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd3, 0x02, 0x0a, 0x14, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x6f, + 0x6e, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x61, 0x6d, 0x65, 0x4f, + 0x6e, 0x6c, 0x79, 0x12, 0x3c, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x53, + 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x6e, 0x73, 0x65, 0x52, 0x04, 0x73, 0x6f, 0x72, + 0x74, 0x12, 0x3b, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x23, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x38, + 0x0a, 0x0c, 0x53, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x6e, 0x73, 0x65, 0x12, 0x0c, + 0x0a, 0x08, 0x55, 0x4e, 0x53, 0x4f, 0x52, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, + 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x55, 0x50, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x4f, 0x52, + 0x54, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x02, 0x22, 0x25, 0x0a, 0x07, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x73, 0x12, 0x0e, 0x0a, 0x0a, 0x55, 0x4e, 0x46, 0x49, 0x4c, 0x54, 0x45, 0x52, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4f, 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x01, 0x22, + 0xfa, 0x01, 0x0a, 0x15, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, + 0x73, 0x12, 0x65, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, + 0x42, 0x4f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x49, 0x12, 0x47, 0x49, 0x66, 0x20, 0x6e, 0x61, 0x6d, + 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x69, 0x73, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, + 0x69, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x20, 0x77, 0x65, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, + 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x20, 0x68, 0x65, 0x72, 0x65, + 0x2e, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x3c, + 0x0a, 0x0b, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x52, 0x0a, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x54, 0x65, 0x72, 0x6d, 0x22, 0x9f, 0x01, 0x0a, + 0x10, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x4f, + 0x0a, 0x0b, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x42, 0x2d, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x27, 0x12, 0x25, 0x49, 0x66, 0x20, + 0x73, 0x65, 0x74, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x20, 0x73, 0x6f, 0x6d, 0x65, 0x20, 0x69, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x52, 0x0b, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, + 0x1d, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x72, 0x75, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x72, 0x75, 0x22, 0x6a, + 0x0a, 0x13, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x49, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x1c, 0x0a, 0x09, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x42, 0x0a, 0x0c, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x3c, + 0x0a, 0x12, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x49, 0x74, 0x65, 0x6d, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x5e, 0x0a, 0x0e, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2f, + 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, + 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x7c, 0x0a, 0x18, + 0x53, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x03, 0x61, 0x64, 0x64, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x49, 0x74, 0x65, 0x6d, + 0x52, 0x03, 0x61, 0x64, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x12, 0x1b, 0x0a, + 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0xa3, 0x03, 0x0a, 0x05, 0x55, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x33, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x2d, 0x12, 0x2b, 0x54, 0x68, + 0x65, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, + 0x6d, 0x20, 0x28, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x7c, 0x44, 0x61, 0x72, 0x77, 0x69, + 0x6e, 0x7c, 0x4c, 0x69, 0x6e, 0x75, 0x78, 0x29, 0x2e, 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, + 0x6d, 0x12, 0x40, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x24, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1e, 0x12, 0x1c, 0x54, 0x68, 0x65, + 0x20, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x69, + 0x73, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x2e, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x50, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x36, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x30, 0x12, 0x2e, 0x54, 0x68, + 0x65, 0x20, 0x4f, 0x53, 0x20, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x20, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x20, 0x65, 0x2e, 0x67, 0x2e, 0x20, 0x37, 0x2c, 0x20, + 0x4f, 0x53, 0x58, 0x2c, 0x20, 0x64, 0x65, 0x62, 0x69, 0x61, 0x6e, 0x2e, 0x52, 0x07, 0x72, 0x65, + 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x33, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x2d, 0x12, 0x2b, + 0x54, 0x68, 0x65, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, 0x61, 0x72, 0x63, 0x68, 0x69, + 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x20, 0x65, 0x2e, 0x67, 0x2e, 0x20, 0x41, 0x4d, 0x44, + 0x36, 0x34, 0x2c, 0x20, 0x78, 0x38, 0x36, 0x5f, 0x36, 0x34, 0x2e, 0x52, 0x07, 0x6d, 0x61, 0x63, + 0x68, 0x69, 0x6e, 0x65, 0x12, 0x45, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x31, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x2b, 0x12, 0x29, 0x54, 0x68, 0x65, 0x20, + 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x27, 0x73, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x79, 0x20, 0x71, + 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x20, + 0x6e, 0x61, 0x6d, 0x65, 0x2e, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x6d, + 0x61, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, + 0x22, 0x39, 0x0a, 0x0b, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, + 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x42, 0x31, 0x5a, 0x2f, 0x77, + 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, + 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1212,37 +1195,40 @@ func file_clients_proto_rawDescGZIP() []byte { return file_clients_proto_rawDescData } -var file_clients_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_clients_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_clients_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_clients_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_clients_proto_goTypes = []interface{}{ - (ApiClient_IPAddressClass)(0), // 0: proto.ApiClient.IPAddressClass - (SearchClientsRequest_SortingSense)(0), // 1: proto.SearchClientsRequest.SortingSense - (SearchClientsRequest_Filters)(0), // 2: proto.SearchClientsRequest.Filters - (*AgentInformation)(nil), // 3: proto.AgentInformation - (*ApiClient)(nil), // 4: proto.ApiClient - (*SearchClientsRequest)(nil), // 5: proto.SearchClientsRequest - (*SearchClientsResponse)(nil), // 6: proto.SearchClientsResponse - (*GetClientRequest)(nil), // 7: proto.GetClientRequest - (*LabelClientsRequest)(nil), // 8: proto.LabelClientsRequest - (*ClientLabels)(nil), // 9: proto.ClientLabels - (*ClientMetadataItem)(nil), // 10: proto.ClientMetadataItem - (*ClientMetadata)(nil), // 11: proto.ClientMetadata + (SearchClientsRequest_SortingSense)(0), // 0: proto.SearchClientsRequest.SortingSense + (SearchClientsRequest_Filters)(0), // 1: proto.SearchClientsRequest.Filters + (*AgentInformation)(nil), // 2: proto.AgentInformation + (*ApiClient)(nil), // 3: proto.ApiClient + (*SearchClientsRequest)(nil), // 4: proto.SearchClientsRequest + (*SearchClientsResponse)(nil), // 5: proto.SearchClientsResponse + (*GetClientRequest)(nil), // 6: proto.GetClientRequest + (*LabelClientsRequest)(nil), // 7: proto.LabelClientsRequest + (*ClientLabels)(nil), // 8: proto.ClientLabels + (*ClientMetadataItem)(nil), // 9: proto.ClientMetadataItem + (*ClientMetadata)(nil), // 10: proto.ClientMetadata + (*SetClientMetadataRequest)(nil), // 11: proto.SetClientMetadataRequest (*Uname)(nil), // 12: proto.Uname (*IndexRecord)(nil), // 13: proto.IndexRecord + nil, // 14: proto.ApiClient.InFlightFlowsEntry } var file_clients_proto_depIdxs = []int32{ - 3, // 0: proto.ApiClient.agent_information:type_name -> proto.AgentInformation + 2, // 0: proto.ApiClient.agent_information:type_name -> proto.AgentInformation 12, // 1: proto.ApiClient.os_info:type_name -> proto.Uname - 0, // 2: proto.ApiClient.last_ip_class:type_name -> proto.ApiClient.IPAddressClass - 1, // 3: proto.SearchClientsRequest.sort:type_name -> proto.SearchClientsRequest.SortingSense - 2, // 4: proto.SearchClientsRequest.filter:type_name -> proto.SearchClientsRequest.Filters - 4, // 5: proto.SearchClientsResponse.items:type_name -> proto.ApiClient - 10, // 6: proto.ClientMetadata.items:type_name -> proto.ClientMetadataItem - 7, // [7:7] is the sub-list for method output_type - 7, // [7:7] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 14, // 2: proto.ApiClient.in_flight_flows:type_name -> proto.ApiClient.InFlightFlowsEntry + 0, // 3: proto.SearchClientsRequest.sort:type_name -> proto.SearchClientsRequest.SortingSense + 1, // 4: proto.SearchClientsRequest.filter:type_name -> proto.SearchClientsRequest.Filters + 3, // 5: proto.SearchClientsResponse.items:type_name -> proto.ApiClient + 4, // 6: proto.SearchClientsResponse.search_term:type_name -> proto.SearchClientsRequest + 9, // 7: proto.ClientMetadata.items:type_name -> proto.ClientMetadataItem + 9, // 8: proto.SetClientMetadataRequest.add:type_name -> proto.ClientMetadataItem + 9, // [9:9] is the sub-list for method output_type + 9, // [9:9] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_clients_proto_init() } @@ -1360,7 +1346,7 @@ func file_clients_proto_init() { } } file_clients_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Uname); i { + switch v := v.(*SetClientMetadataRequest); i { case 0: return &v.state case 1: @@ -1372,6 +1358,18 @@ func file_clients_proto_init() { } } file_clients_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Uname); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_clients_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*IndexRecord); i { case 0: return &v.state @@ -1389,8 +1387,8 @@ func file_clients_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_clients_proto_rawDesc, - NumEnums: 3, - NumMessages: 11, + NumEnums: 2, + NumMessages: 13, NumExtensions: 0, NumServices: 0, }, diff --git a/api/proto/clients.proto b/api/proto/clients.proto index 2b4109110..f9982443b 100644 --- a/api/proto/clients.proto +++ b/api/proto/clients.proto @@ -13,10 +13,15 @@ message AgentInformation { string version = 1; string name = 2; string build_time = 3; + string build_url = 4; } // Describe a client. We fill in some metadata about the client but // this is by no means exhaustive. + +// TODO: This is an older protobuf that is now largely supeceeded by +// actions_proto.ClientInfo. We need to replace use of this protobuf +// by ClientInfo. message ApiClient { string client_id = 1 [(sem_type) = { type: "ApiClientId", @@ -33,18 +38,6 @@ message ApiClient { type: "RDFDatetime", description: "Last time when client checked in." }]; - uint64 last_booted_at = 8 [(sem_type) = { - type: "RDFDatetime", - description: "Last boot time." - }]; - uint64 last_clock = 9 [(sem_type) = { - type: "RDFDatetime", - description: "Client clocks value during latest checkin." - }]; - uint64 last_crash_at = 10 [(sem_type) = { - type: "RDFDatetime", - description: "Last crash time." - }]; string last_ip = 16 [(sem_type) = { description: "The last seen remote API address" @@ -53,16 +46,15 @@ message ApiClient { string last_interrogate_flow_id = 19; string last_interrogate_artifact_name = 21; - enum IPAddressClass { - UNKNOWN = 0; - INTERNAL = 1; - EXTERNAL = 2; - VPN = 3; - } + repeated string labels = 18; - IPAddressClass last_ip_class = 17; + uint64 last_hunt_timestamp = 22; + uint64 last_event_table_version = 23; - repeated string labels = 18; + // Last time the labels on this client were updated. + uint64 last_label_timestamp = 24; + + map in_flight_flows = 25; } message SearchClientsRequest { @@ -97,6 +89,8 @@ message SearchClientsResponse { description: "If name_only is specified in the request we only " "return the names here.", }]; + uint64 total = 3; + SearchClientsRequest search_term = 4; } message GetClientRequest { @@ -135,6 +129,14 @@ message ClientMetadata { string client_id = 2; } +message SetClientMetadataRequest { + repeated ClientMetadataItem add = 1; + + // A list of keys to remove + repeated string remove = 2; + string client_id = 3; +} + // Message to carry uname information. @@ -148,33 +150,14 @@ message Uname { string release = 3 [(sem_type) = { description: "The OS release identifier e.g. 7, OSX, debian.", }]; - string version = 4 [(sem_type) = { - description: "The OS version ID e.g. 6.1.7601SP1, 10.9.2, 14.04.", - }]; string machine = 5 [(sem_type) = { description: "The system architecture e.g. AMD64, x86_64.", }]; - string kernel = 6 [(sem_type) = { - description: "The kernel version string e.g. 6.1.7601, 13.1.0, 3.15-rc2.", - }]; string fqdn = 7 [(sem_type) = { description: "The system's fully qualified domain name.", }]; - uint64 install_date = 8 [(sem_type) = { - type: "RDFDatetime", - description: "When system was installed." - }]; - - string libc_ver = 9 [(sem_type) = { - description: "The C library version", - }]; - - string architecture = 10 [(sem_type) = { - description: "The architecture of this binary. (Note this can be " - "different from the machine architecture in the case of a 32 bit binary " - "running on a 64 bit system)", - }]; + repeated string mac_addresses = 11; }; diff --git a/api/proto/completions.pb.go b/api/proto/completions.pb.go index d9c523e24..6ff22de9f 100644 --- a/api/proto/completions.pb.go +++ b/api/proto/completions.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: completions.proto package proto @@ -104,11 +101,15 @@ type Completion struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` - Args []*ArgDescriptor `protobuf:"bytes,4,rep,name=args,proto3" json:"args,omitempty"` - Category string `protobuf:"bytes,5,opt,name=category,proto3" json:"category,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Version uint64 `protobuf:"varint,6,opt,name=version,proto3" json:"version,omitempty"` + Args []*ArgDescriptor `protobuf:"bytes,4,rep,name=args,proto3" json:"args,omitempty"` + Category string `protobuf:"bytes,5,opt,name=category,proto3" json:"category,omitempty"` + Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Platforms []string `protobuf:"bytes,8,rep,name=platforms,proto3" json:"platforms,omitempty"` + FreeFormArgs bool `protobuf:"varint,9,opt,name=free_form_args,json=freeFormArgs,proto3" json:"free_form_args,omitempty"` } func (x *Completion) Reset() { @@ -164,6 +165,13 @@ func (x *Completion) GetType() string { return "" } +func (x *Completion) GetVersion() uint64 { + if x != nil { + return x.Version + } + return 0 +} + func (x *Completion) GetArgs() []*ArgDescriptor { if x != nil { return x.Args @@ -178,6 +186,27 @@ func (x *Completion) GetCategory() string { return "" } +func (x *Completion) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Completion) GetPlatforms() []string { + if x != nil { + return x.Platforms + } + return nil +} + +func (x *Completion) GetFreeFormArgs() bool { + if x != nil { + return x.FreeFormArgs + } + return false +} + type KeywordCompletions struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -238,25 +267,38 @@ var file_completions_proto_rawDesc = []byte{ 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x9c, - 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0xf4, + 0x02, 0x0a, 0x0a, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, - 0x67, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x04, 0x61, 0x72, 0x67, - 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x22, 0x3d, 0x0a, - 0x12, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x42, 0x31, 0x5a, 0x2f, - 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, - 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x28, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x67, 0x44, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x63, + 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, + 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, + 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, + 0x6d, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x5f, + 0x61, 0x72, 0x67, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x66, 0x72, 0x65, 0x65, + 0x46, 0x6f, 0x72, 0x6d, 0x41, 0x72, 0x67, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3d, 0x0a, 0x12, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, + 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, + 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, + 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -271,20 +313,22 @@ func file_completions_proto_rawDescGZIP() []byte { return file_completions_proto_rawDescData } -var file_completions_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_completions_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_completions_proto_goTypes = []interface{}{ (*ArgDescriptor)(nil), // 0: proto.ArgDescriptor (*Completion)(nil), // 1: proto.Completion (*KeywordCompletions)(nil), // 2: proto.KeywordCompletions + nil, // 3: proto.Completion.MetadataEntry } var file_completions_proto_depIdxs = []int32{ 0, // 0: proto.Completion.args:type_name -> proto.ArgDescriptor - 1, // 1: proto.KeywordCompletions.items:type_name -> proto.Completion - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 3, // 1: proto.Completion.metadata:type_name -> proto.Completion.MetadataEntry + 1, // 2: proto.KeywordCompletions.items:type_name -> proto.Completion + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_completions_proto_init() } @@ -336,7 +380,7 @@ func file_completions_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_completions_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/api/proto/completions.proto b/api/proto/completions.proto index 9f2345f29..195ed7028 100644 --- a/api/proto/completions.proto +++ b/api/proto/completions.proto @@ -16,8 +16,12 @@ message Completion { string name = 1; string description = 2; string type = 3; + uint64 version = 6; repeated ArgDescriptor args = 4; string category = 5; + map metadata = 7; + repeated string platforms = 8; + bool free_form_args = 9; } message KeywordCompletions { diff --git a/api/proto/csv.pb.go b/api/proto/csv.pb.go index 494afe264..9543609a8 100644 --- a/api/proto/csv.pb.go +++ b/api/proto/csv.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: csv.proto package proto @@ -13,6 +10,7 @@ import ( sync "sync" proto "www.velocidex.com/golang/velociraptor/artifacts/proto" _ "www.velocidex.com/golang/velociraptor/proto" + proto1 "www.velocidex.com/golang/velociraptor/timelines/proto" ) const ( @@ -34,8 +32,8 @@ type GetTableRequest struct { // artifacts should specify the artifact name with client_id being // either "server" for server events or the client id for the // client events. Number of seconds since epoch. - StartTime uint64 `protobuf:"varint,13,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` - EndTime uint64 `protobuf:"varint,14,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` + StartTime int64 `protobuf:"varint,13,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + EndTime int64 `protobuf:"varint,14,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` // For collected artifacts tables. ClientId string `protobuf:"bytes,4,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` FlowId string `protobuf:"bytes,5,opt,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` @@ -45,13 +43,15 @@ type GetTableRequest struct { // For collected hunts. With hunts, type can be clients, hunt_status. HuntId string `protobuf:"bytes,8,opt,name=hunt_id,json=huntId,proto3" json:"hunt_id,omitempty"` // For notebook tables. - NotebookId string `protobuf:"bytes,9,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` - CellId string `protobuf:"bytes,10,opt,name=cell_id,json=cellId,proto3" json:"cell_id,omitempty"` - TableId int64 `protobuf:"varint,11,opt,name=table_id,json=tableId,proto3" json:"table_id,omitempty"` + NotebookId string `protobuf:"bytes,9,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` + CellId string `protobuf:"bytes,10,opt,name=cell_id,json=cellId,proto3" json:"cell_id,omitempty"` + CellVersion string `protobuf:"bytes,29,opt,name=cell_version,json=cellVersion,proto3" json:"cell_version,omitempty"` + TableId int64 `protobuf:"varint,11,opt,name=table_id,json=tableId,proto3" json:"table_id,omitempty"` // For timelines Timeline string `protobuf:"bytes,16,opt,name=timeline,proto3" json:"timeline,omitempty"` // Skip these timeline components. - SkipComponents []string `protobuf:"bytes,17,rep,name=skip_components,json=skipComponents,proto3" json:"skip_components,omitempty"` + IncludeComponents []string `protobuf:"bytes,31,rep,name=include_components,json=includeComponents,proto3" json:"include_components,omitempty"` + SkipComponents []string `protobuf:"bytes,17,rep,name=skip_components,json=skipComponents,proto3" json:"skip_components,omitempty"` // For download handler when creating an export file - control // output format. Can be "csv", "jsonl" DownloadFormat string `protobuf:"bytes,12,opt,name=download_format,json=downloadFormat,proto3" json:"download_format,omitempty"` @@ -59,6 +59,27 @@ type GetTableRequest struct { DownloadFilename string `protobuf:"bytes,18,opt,name=download_filename,json=downloadFilename,proto3" json:"download_filename,omitempty"` // If specified only emit these columns. Columns []string `protobuf:"bytes,15,rep,name=columns,proto3" json:"columns,omitempty"` + // If specified, transform the table first. + SortColumn string `protobuf:"bytes,19,opt,name=sort_column,json=sortColumn,proto3" json:"sort_column,omitempty"` + SortDirection bool `protobuf:"varint,20,opt,name=sort_direction,json=sortDirection,proto3" json:"sort_direction,omitempty"` + FilterColumn string `protobuf:"bytes,21,opt,name=filter_column,json=filterColumn,proto3" json:"filter_column,omitempty"` + FilterRegex string `protobuf:"bytes,22,opt,name=filter_regex,json=filterRegex,proto3" json:"filter_regex,omitempty"` + // Set with the output of GetTableResponse to view the stack + // table. If this is set the transform options above refer to the + // stack table itself. + StackPath []string `protobuf:"bytes,30,rep,name=stack_path,json=stackPath,proto3" json:"stack_path,omitempty"` + // This transformation takes a range from the larger result set + // and pages within that range. + StartIdx uint64 `protobuf:"varint,27,opt,name=start_idx,json=startIdx,proto3" json:"start_idx,omitempty"` + EndIdx uint64 `protobuf:"varint,28,opt,name=end_idx,json=endIdx,proto3" json:"end_idx,omitempty"` + // The org id may be specified in the query string - The protobuf + // is normally parsed from the query string directly. + OrgId string `protobuf:"bytes,23,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + // The required timezone to export in. + Timezone string `protobuf:"bytes,24,opt,name=timezone,proto3" json:"timezone,omitempty"` + Version uint64 `protobuf:"varint,25,opt,name=version,proto3" json:"version,omitempty"` + // Used for VFS components + VfsComponents []string `protobuf:"bytes,26,rep,name=vfs_components,json=vfsComponents,proto3" json:"vfs_components,omitempty"` } func (x *GetTableRequest) Reset() { @@ -107,14 +128,14 @@ func (x *GetTableRequest) GetStartRow() uint64 { return 0 } -func (x *GetTableRequest) GetStartTime() uint64 { +func (x *GetTableRequest) GetStartTime() int64 { if x != nil { return x.StartTime } return 0 } -func (x *GetTableRequest) GetEndTime() uint64 { +func (x *GetTableRequest) GetEndTime() int64 { if x != nil { return x.EndTime } @@ -170,6 +191,13 @@ func (x *GetTableRequest) GetCellId() string { return "" } +func (x *GetTableRequest) GetCellVersion() string { + if x != nil { + return x.CellVersion + } + return "" +} + func (x *GetTableRequest) GetTableId() int64 { if x != nil { return x.TableId @@ -184,6 +212,13 @@ func (x *GetTableRequest) GetTimeline() string { return "" } +func (x *GetTableRequest) GetIncludeComponents() []string { + if x != nil { + return x.IncludeComponents + } + return nil +} + func (x *GetTableRequest) GetSkipComponents() []string { if x != nil { return x.SkipComponents @@ -212,12 +247,91 @@ func (x *GetTableRequest) GetColumns() []string { return nil } +func (x *GetTableRequest) GetSortColumn() string { + if x != nil { + return x.SortColumn + } + return "" +} + +func (x *GetTableRequest) GetSortDirection() bool { + if x != nil { + return x.SortDirection + } + return false +} + +func (x *GetTableRequest) GetFilterColumn() string { + if x != nil { + return x.FilterColumn + } + return "" +} + +func (x *GetTableRequest) GetFilterRegex() string { + if x != nil { + return x.FilterRegex + } + return "" +} + +func (x *GetTableRequest) GetStackPath() []string { + if x != nil { + return x.StackPath + } + return nil +} + +func (x *GetTableRequest) GetStartIdx() uint64 { + if x != nil { + return x.StartIdx + } + return 0 +} + +func (x *GetTableRequest) GetEndIdx() uint64 { + if x != nil { + return x.EndIdx + } + return 0 +} + +func (x *GetTableRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *GetTableRequest) GetTimezone() string { + if x != nil { + return x.Timezone + } + return "" +} + +func (x *GetTableRequest) GetVersion() uint64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *GetTableRequest) GetVfsComponents() []string { + if x != nil { + return x.VfsComponents + } + return nil +} + type Row struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Cell []string `protobuf:"bytes,1,rep,name=cell,proto3" json:"cell,omitempty"` + // The row encoded as a JSON array: Current code encodes the + // entire JSON []interface{} into a single JSON string. + Json string `protobuf:"bytes,2,opt,name=json,proto3" json:"json,omitempty"` } func (x *Row) Reset() { @@ -252,11 +366,11 @@ func (*Row) Descriptor() ([]byte, []int) { return file_csv_proto_rawDescGZIP(), []int{1} } -func (x *Row) GetCell() []string { +func (x *Row) GetJson() string { if x != nil { - return x.Cell + return x.Json } - return nil + return "" } type GetTableResponse struct { @@ -268,8 +382,12 @@ type GetTableResponse struct { Rows []*Row `protobuf:"bytes,2,rep,name=rows,proto3" json:"rows,omitempty"` TotalRows int64 `protobuf:"varint,3,opt,name=total_rows,json=totalRows,proto3" json:"total_rows,omitempty"` ColumnTypes []*proto.ColumnType `protobuf:"bytes,4,rep,name=column_types,json=columnTypes,proto3" json:"column_types,omitempty"` - StartTime int64 `protobuf:"varint,5,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` - EndTime int64 `protobuf:"varint,6,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` + // Used for time based data. + StartTime int64 `protobuf:"varint,5,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + EndTime int64 `protobuf:"varint,6,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` + Timelines []*proto1.Timeline `protobuf:"bytes,8,rep,name=timelines,proto3" json:"timelines,omitempty"` + // If there is a stack table this is the path to it. + StackPath []string `protobuf:"bytes,7,rep,name=stack_path,json=stackPath,proto3" json:"stack_path,omitempty"` } func (x *GetTableResponse) Reset() { @@ -346,6 +464,20 @@ func (x *GetTableResponse) GetEndTime() int64 { return 0 } +func (x *GetTableResponse) GetTimelines() []*proto1.Timeline { + if x != nil { + return x.Timelines + } + return nil +} + +func (x *GetTableResponse) GetStackPath() []string { + if x != nil { + return x.StackPath + } + return nil +} + var File_csv_proto protoreflect.FileDescriptor var file_csv_proto_rawDesc = []byte{ @@ -353,60 +485,94 @@ var file_csv_proto_rawDesc = []byte{ 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x85, 0x04, 0x0a, 0x0f, 0x47, 0x65, 0x74, - 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, - 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, - 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x6f, 0x77, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x52, 0x6f, 0x77, 0x12, 0x1d, 0x0a, - 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, - 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, - 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x1a, 0x0a, - 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, - 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, - 0x6f, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x6f, 0x74, - 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x65, 0x6c, 0x6c, 0x5f, - 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x65, 0x6c, 0x6c, 0x49, 0x64, - 0x12, 0x19, 0x0a, 0x08, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x74, - 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, - 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x6b, 0x69, 0x70, 0x5f, - 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0e, 0x73, 0x6b, 0x69, 0x70, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, - 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x6f, 0x77, 0x6e, 0x6c, - 0x6f, 0x61, 0x64, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x6f, 0x77, - 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x12, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, - 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, - 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, - 0x22, 0x19, 0x0a, 0x03, 0x52, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x6c, 0x6c, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x63, 0x65, 0x6c, 0x6c, 0x22, 0xf0, 0x01, 0x0a, 0x10, - 0x47, 0x65, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x2d, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x09, 0x42, 0x13, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x0d, 0x12, 0x0b, 0x54, 0x68, 0x65, 0x20, 0x63, - 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x12, - 0x1e, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x6f, 0x77, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12, - 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x34, - 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6f, 0x6c, - 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, - 0x79, 0x70, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, - 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x31, - 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, - 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, + 0x6e, 0x65, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, + 0x6e, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb0, 0x07, 0x0a, 0x0f, 0x47, 0x65, + 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, + 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x72, 0x6f, 0x77, + 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x6f, 0x77, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x52, 0x6f, 0x77, 0x12, 0x1d, + 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, + 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x1a, + 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x17, + 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x74, 0x65, 0x62, + 0x6f, 0x6f, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x6f, + 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x65, 0x6c, 0x6c, + 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x65, 0x6c, 0x6c, 0x49, + 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x65, 0x6c, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x65, 0x6c, 0x6c, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x49, 0x64, 0x12, + 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x69, + 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, + 0x73, 0x18, 0x1f, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, + 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x6b, + 0x69, 0x70, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x11, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x6b, 0x69, 0x70, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, + 0x6e, 0x74, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x6f, + 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x2b, 0x0a, 0x11, + 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, + 0x64, 0x46, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6c, + 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, + 0x6d, 0x6e, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x63, 0x6f, 0x6c, 0x75, + 0x6d, 0x6e, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x43, 0x6f, + 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x64, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x6f, + 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x15, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, + 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, + 0x18, 0x16, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x65, + 0x67, 0x65, 0x78, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x70, 0x61, 0x74, + 0x68, 0x18, 0x1e, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x50, 0x61, + 0x74, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x78, 0x18, + 0x1b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x49, 0x64, 0x78, 0x12, + 0x17, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x78, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x06, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x78, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, + 0x69, 0x64, 0x18, 0x17, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x12, + 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x18, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x19, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x66, 0x73, 0x5f, 0x63, 0x6f, 0x6d, + 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x1a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x76, + 0x66, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x19, 0x0a, 0x03, + 0x52, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x22, 0xbe, 0x02, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x54, + 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x07, + 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x42, 0x13, 0xe2, + 0xfc, 0xe3, 0xc4, 0x01, 0x0d, 0x12, 0x0b, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6c, 0x75, 0x6d, + 0x6e, 0x73, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x04, 0x72, + 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x52, 0x6f, 0x77, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x34, 0x0a, 0x0c, 0x63, 0x6f, + 0x6c, 0x75, 0x6d, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x73, + 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x09, 0x74, 0x69, + 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x52, 0x09, + 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x73, + 0x74, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x74, 0x68, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, + 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, + 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, + 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -427,15 +593,17 @@ var file_csv_proto_goTypes = []interface{}{ (*Row)(nil), // 1: proto.Row (*GetTableResponse)(nil), // 2: proto.GetTableResponse (*proto.ColumnType)(nil), // 3: proto.ColumnType + (*proto1.Timeline)(nil), // 4: proto.Timeline } var file_csv_proto_depIdxs = []int32{ 1, // 0: proto.GetTableResponse.rows:type_name -> proto.Row 3, // 1: proto.GetTableResponse.column_types:type_name -> proto.ColumnType - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 4, // 2: proto.GetTableResponse.timelines:type_name -> proto.Timeline + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_csv_proto_init() } diff --git a/api/proto/csv.proto b/api/proto/csv.proto index 8778df255..d5407b872 100644 --- a/api/proto/csv.proto +++ b/api/proto/csv.proto @@ -2,6 +2,7 @@ syntax = "proto3"; import "proto/semantic.proto"; import "artifacts/proto/artifact.proto"; +import "timelines/proto/timelines.proto"; package proto; @@ -19,8 +20,8 @@ message GetTableRequest { // artifacts should specify the artifact name with client_id being // either "server" for server events or the client id for the // client events. Number of seconds since epoch. - uint64 start_time = 13; - uint64 end_time = 14; + int64 start_time = 13; + int64 end_time = 14; // For collected artifacts tables. string client_id = 4; @@ -36,11 +37,15 @@ message GetTableRequest { // For notebook tables. string notebook_id = 9; string cell_id = 10; + string cell_version = 29; + int64 table_id = 11; // For timelines string timeline = 16; + // Skip these timeline components. + repeated string include_components = 31; repeated string skip_components = 17; // For download handler when creating an export file - control @@ -52,10 +57,43 @@ message GetTableRequest { // If specified only emit these columns. repeated string columns = 15; + + // If specified, transform the table first. + string sort_column = 19; + bool sort_direction = 20; + string filter_column = 21; + string filter_regex = 22; + + // Set with the output of GetTableResponse to view the stack + // table. If this is set the transform options above refer to the + // stack table itself. + repeated string stack_path = 30; + + // This transformation takes a range from the larger result set + // and pages within that range. + uint64 start_idx = 27; + uint64 end_idx = 28; + + // The org id may be specified in the query string - The protobuf + // is normally parsed from the query string directly. + string org_id = 23; + + // The required timezone to export in. + string timezone = 24; + + uint64 version = 25; + + // Used for VFS components + repeated string vfs_components = 26; } message Row { - repeated string cell = 1; + // Deprecated - Old code serializes each cell separately. + // repeated string cell = 1; + + // The row encoded as a JSON array: Current code encodes the + // entire JSON []interface{} into a single JSON string. + string json = 2; } message GetTableResponse { @@ -69,6 +107,12 @@ message GetTableResponse { repeated ColumnType column_types = 4; + // Used for time based data. int64 start_time = 5; int64 end_time = 6; + + repeated Timeline timelines = 8; + + // If there is a stack table this is the path to it. + repeated string stack_path = 7; } \ No newline at end of file diff --git a/api/proto/datastore.pb.go b/api/proto/datastore.pb.go index d2c261930..4e8a1a1d9 100644 --- a/api/proto/datastore.pb.go +++ b/api/proto/datastore.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: datastore.proto package proto @@ -100,7 +97,8 @@ type DataRequest struct { Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` // If set the request will block until the data is committed to // disk. - Sync bool `protobuf:"varint,3,opt,name=sync,proto3" json:"sync,omitempty"` + Sync bool `protobuf:"varint,3,opt,name=sync,proto3" json:"sync,omitempty"` + OrgId string `protobuf:"bytes,4,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` } func (x *DataRequest) Reset() { @@ -156,6 +154,13 @@ func (x *DataRequest) GetSync() bool { return false } +func (x *DataRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + type DataResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -261,24 +266,25 @@ var file_datastore_proto_rawDesc = []byte{ 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x74, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x69, 0x73, 0x5f, 0x64, 0x69, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x69, 0x73, 0x44, 0x69, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, - 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x22, 0x64, 0x0a, 0x0b, + 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x22, 0x7b, 0x0a, 0x0b, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x08, 0x70, 0x61, 0x74, 0x68, 0x73, 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x53, 0x50, 0x61, 0x74, 0x68, 0x53, 0x70, 0x65, 0x63, 0x52, 0x08, 0x70, 0x61, 0x74, 0x68, 0x73, 0x70, 0x65, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x79, 0x6e, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x73, 0x79, - 0x6e, 0x63, 0x22, 0x22, 0x0a, 0x0c, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x45, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, - 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, - 0x0a, 0x08, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x53, 0x50, 0x61, 0x74, 0x68, 0x53, - 0x70, 0x65, 0x63, 0x52, 0x08, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x42, 0x31, 0x5a, - 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, - 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x63, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0x22, 0x0a, 0x0c, 0x44, 0x61, 0x74, + 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x45, 0x0a, + 0x14, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x08, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x72, 0x65, + 0x6e, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x44, 0x53, 0x50, 0x61, 0x74, 0x68, 0x53, 0x70, 0x65, 0x63, 0x52, 0x08, 0x63, 0x68, 0x69, 0x6c, + 0x64, 0x72, 0x65, 0x6e, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, + 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, + 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/api/proto/datastore.proto b/api/proto/datastore.proto index c129d8163..1dba73dcb 100644 --- a/api/proto/datastore.proto +++ b/api/proto/datastore.proto @@ -19,6 +19,8 @@ message DataRequest { // If set the request will block until the data is committed to // disk. bool sync = 3; + + string org_id = 4; } message DataResponse { diff --git a/api/proto/docs.pb.go b/api/proto/docs.pb.go new file mode 100644 index 000000000..11f875726 --- /dev/null +++ b/api/proto/docs.pb.go @@ -0,0 +1,439 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: docs.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DocSearchRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + Start int64 `protobuf:"varint,2,opt,name=start,proto3" json:"start,omitempty"` + Length int64 `protobuf:"varint,3,opt,name=length,proto3" json:"length,omitempty"` +} + +func (x *DocSearchRequest) Reset() { + *x = DocSearchRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_docs_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DocSearchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DocSearchRequest) ProtoMessage() {} + +func (x *DocSearchRequest) ProtoReflect() protoreflect.Message { + mi := &file_docs_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DocSearchRequest.ProtoReflect.Descriptor instead. +func (*DocSearchRequest) Descriptor() ([]byte, []int) { + return file_docs_proto_rawDescGZIP(), []int{0} +} + +func (x *DocSearchRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *DocSearchRequest) GetStart() int64 { + if x != nil { + return x.Start + } + return 0 +} + +func (x *DocSearchRequest) GetLength() int64 { + if x != nil { + return x.Length + } + return 0 +} + +type Highlight struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Start uint64 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` + End uint64 `protobuf:"varint,2,opt,name=end,proto3" json:"end,omitempty"` +} + +func (x *Highlight) Reset() { + *x = Highlight{} + if protoimpl.UnsafeEnabled { + mi := &file_docs_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Highlight) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Highlight) ProtoMessage() {} + +func (x *Highlight) ProtoReflect() protoreflect.Message { + mi := &file_docs_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Highlight.ProtoReflect.Descriptor instead. +func (*Highlight) Descriptor() ([]byte, []int) { + return file_docs_proto_rawDescGZIP(), []int{1} +} + +func (x *Highlight) GetStart() uint64 { + if x != nil { + return x.Start + } + return 0 +} + +func (x *Highlight) GetEnd() uint64 { + if x != nil { + return x.End + } + return 0 +} + +type DocSearchResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Link string `protobuf:"bytes,1,opt,name=link,proto3" json:"link,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Fragment string `protobuf:"bytes,4,opt,name=fragment,proto3" json:"fragment,omitempty"` + FullText string `protobuf:"bytes,5,opt,name=full_text,json=fullText,proto3" json:"full_text,omitempty"` + Highlights []*Highlight `protobuf:"bytes,6,rep,name=highlights,proto3" json:"highlights,omitempty"` + Tags []string `protobuf:"bytes,7,rep,name=tags,proto3" json:"tags,omitempty"` + // JSON encoded crumbs structure + Crumbs string `protobuf:"bytes,8,opt,name=crumbs,proto3" json:"crumbs,omitempty"` +} + +func (x *DocSearchResponse) Reset() { + *x = DocSearchResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_docs_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DocSearchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DocSearchResponse) ProtoMessage() {} + +func (x *DocSearchResponse) ProtoReflect() protoreflect.Message { + mi := &file_docs_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DocSearchResponse.ProtoReflect.Descriptor instead. +func (*DocSearchResponse) Descriptor() ([]byte, []int) { + return file_docs_proto_rawDescGZIP(), []int{2} +} + +func (x *DocSearchResponse) GetLink() string { + if x != nil { + return x.Link + } + return "" +} + +func (x *DocSearchResponse) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *DocSearchResponse) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *DocSearchResponse) GetFragment() string { + if x != nil { + return x.Fragment + } + return "" +} + +func (x *DocSearchResponse) GetFullText() string { + if x != nil { + return x.FullText + } + return "" +} + +func (x *DocSearchResponse) GetHighlights() []*Highlight { + if x != nil { + return x.Highlights + } + return nil +} + +func (x *DocSearchResponse) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *DocSearchResponse) GetCrumbs() string { + if x != nil { + return x.Crumbs + } + return "" +} + +type DocSearchResponses struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"` + Items []*DocSearchResponse `protobuf:"bytes,2,rep,name=Items,proto3" json:"Items,omitempty"` +} + +func (x *DocSearchResponses) Reset() { + *x = DocSearchResponses{} + if protoimpl.UnsafeEnabled { + mi := &file_docs_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DocSearchResponses) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DocSearchResponses) ProtoMessage() {} + +func (x *DocSearchResponses) ProtoReflect() protoreflect.Message { + mi := &file_docs_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DocSearchResponses.ProtoReflect.Descriptor instead. +func (*DocSearchResponses) Descriptor() ([]byte, []int) { + return file_docs_proto_rawDescGZIP(), []int{3} +} + +func (x *DocSearchResponses) GetTotal() uint64 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *DocSearchResponses) GetItems() []*DocSearchResponse { + if x != nil { + return x.Items + } + return nil +} + +var File_docs_proto protoreflect.FileDescriptor + +var file_docs_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x64, 0x6f, 0x63, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0x56, 0x0a, 0x10, 0x44, 0x6f, 0x63, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0x33, 0x0a, 0x09, 0x48, + 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, + 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x65, 0x6e, 0x64, + 0x22, 0xe8, 0x01, 0x0a, 0x11, 0x44, 0x6f, 0x63, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, + 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, + 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x12, 0x30, 0x0a, + 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x69, 0x67, 0x68, 0x6c, 0x69, + 0x67, 0x68, 0x74, 0x52, 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, + 0x61, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x72, 0x75, 0x6d, 0x62, 0x73, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x72, 0x75, 0x6d, 0x62, 0x73, 0x22, 0x5a, 0x0a, 0x12, 0x44, + 0x6f, 0x63, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x2e, 0x0a, 0x05, 0x49, 0x74, 0x65, 0x6d, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, + 0x6f, 0x63, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x52, 0x05, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, + 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, + 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_docs_proto_rawDescOnce sync.Once + file_docs_proto_rawDescData = file_docs_proto_rawDesc +) + +func file_docs_proto_rawDescGZIP() []byte { + file_docs_proto_rawDescOnce.Do(func() { + file_docs_proto_rawDescData = protoimpl.X.CompressGZIP(file_docs_proto_rawDescData) + }) + return file_docs_proto_rawDescData +} + +var file_docs_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_docs_proto_goTypes = []interface{}{ + (*DocSearchRequest)(nil), // 0: proto.DocSearchRequest + (*Highlight)(nil), // 1: proto.Highlight + (*DocSearchResponse)(nil), // 2: proto.DocSearchResponse + (*DocSearchResponses)(nil), // 3: proto.DocSearchResponses +} +var file_docs_proto_depIdxs = []int32{ + 1, // 0: proto.DocSearchResponse.highlights:type_name -> proto.Highlight + 2, // 1: proto.DocSearchResponses.Items:type_name -> proto.DocSearchResponse + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_docs_proto_init() } +func file_docs_proto_init() { + if File_docs_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_docs_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DocSearchRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_docs_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Highlight); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_docs_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DocSearchResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_docs_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DocSearchResponses); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_docs_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_docs_proto_goTypes, + DependencyIndexes: file_docs_proto_depIdxs, + MessageInfos: file_docs_proto_msgTypes, + }.Build() + File_docs_proto = out.File + file_docs_proto_rawDesc = nil + file_docs_proto_goTypes = nil + file_docs_proto_depIdxs = nil +} diff --git a/api/proto/docs.proto b/api/proto/docs.proto new file mode 100644 index 000000000..d23871590 --- /dev/null +++ b/api/proto/docs.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package proto; + +option go_package = "www.velocidex.com/golang/velociraptor/api/proto"; + +message DocSearchRequest { + string query = 1; + int64 start = 2; + int64 length = 3; +} + +message Highlight { + uint64 start = 1; + uint64 end = 2; +} + +message DocSearchResponse { + string link = 1; + string title = 2; + string type = 3; + string fragment = 4; + string full_text = 5; + repeated Highlight highlights = 6; + repeated string tags = 7; + + // JSON encoded crumbs structure + string crumbs = 8; +} + +message DocSearchResponses { + uint64 total = 1; + repeated DocSearchResponse Items = 2; +} \ No newline at end of file diff --git a/api/proto/download.pb.go b/api/proto/download.pb.go index 204baed24..cf65a2f12 100644 --- a/api/proto/download.pb.go +++ b/api/proto/download.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: download.proto package proto @@ -34,9 +31,11 @@ type CreateDownloadRequest struct { JsonFormat bool `protobuf:"varint,5,opt,name=json_format,json=jsonFormat,proto3" json:"json_format,omitempty"` CsvFormat bool `protobuf:"varint,6,opt,name=csv_format,json=csvFormat,proto3" json:"csv_format,omitempty"` // Can be "report" for html report or "" for just files. - DownloadType string `protobuf:"bytes,7,opt,name=download_type,json=downloadType,proto3" json:"download_type,omitempty"` + DownloadType string `protobuf:"bytes,7,opt,name=download_type,json=downloadType,proto3" json:"download_type,omitempty"` // DEPRECATED // If set we lock the file with this password. Password string `protobuf:"bytes,8,opt,name=password,proto3" json:"password,omitempty"` + // If set we expand all sparse files in the archive. + ExpandSparse bool `protobuf:"varint,9,opt,name=expand_sparse,json=expandSparse,proto3" json:"expand_sparse,omitempty"` } func (x *CreateDownloadRequest) Reset() { @@ -127,6 +126,13 @@ func (x *CreateDownloadRequest) GetPassword() string { return "" } +func (x *CreateDownloadRequest) GetExpandSparse() bool { + if x != nil { + return x.ExpandSparse + } + return false +} + type CreateDownloadResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -181,6 +187,8 @@ type FormUploadMetadata struct { Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + // A list of Path components to the VFS + VfsPath []string `protobuf:"bytes,3,rep,name=VfsPath,proto3" json:"VfsPath,omitempty"` } func (x *FormUploadMetadata) Reset() { @@ -229,11 +237,18 @@ func (x *FormUploadMetadata) GetUrl() string { return "" } +func (x *FormUploadMetadata) GetVfsPath() []string { + if x != nil { + return x.VfsPath + } + return nil +} + var File_download_proto protoreflect.FileDescriptor var file_download_proto_rawDesc = []byte{ 0x0a, 0x0e, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x95, 0x02, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, + 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xba, 0x02, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, @@ -250,19 +265,23 @@ var file_download_proto_rawDesc = []byte{ 0x0a, 0x0d, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, - 0x33, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, - 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x76, 0x66, 0x73, - 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x66, 0x73, - 0x50, 0x61, 0x74, 0x68, 0x22, 0x42, 0x0a, 0x12, 0x46, 0x6f, 0x72, 0x6d, 0x55, 0x70, 0x6c, 0x6f, - 0x61, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, - 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, - 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, - 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, - 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, - 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, + 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x5f, 0x73, 0x70, 0x61, 0x72, 0x73, 0x65, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x53, 0x70, + 0x61, 0x72, 0x73, 0x65, 0x22, 0x33, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, + 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, + 0x0a, 0x08, 0x76, 0x66, 0x73, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x76, 0x66, 0x73, 0x50, 0x61, 0x74, 0x68, 0x22, 0x5c, 0x0a, 0x12, 0x46, 0x6f, 0x72, + 0x6d, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, + 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, + 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x18, 0x0a, + 0x07, 0x56, 0x66, 0x73, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, + 0x56, 0x66, 0x73, 0x50, 0x61, 0x74, 0x68, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, + 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, + 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/api/proto/download.proto b/api/proto/download.proto index a50a98772..2cd4c2967 100644 --- a/api/proto/download.proto +++ b/api/proto/download.proto @@ -18,10 +18,13 @@ message CreateDownloadRequest { bool csv_format = 6; // Can be "report" for html report or "" for just files. - string download_type = 7; + string download_type = 7; // DEPRECATED // If set we lock the file with this password. string password = 8; + + // If set we expand all sparse files in the archive. + bool expand_sparse = 9; } message CreateDownloadResponse { @@ -31,4 +34,7 @@ message CreateDownloadResponse { message FormUploadMetadata { string filename = 1; string url = 2; + + // A list of Path components to the VFS + repeated string VfsPath = 3; } \ No newline at end of file diff --git a/api/proto/flows.pb.go b/api/proto/flows.pb.go index 6954dd82a..2a4e73beb 100644 --- a/api/proto/flows.pb.go +++ b/api/proto/flows.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: flows.proto package proto @@ -22,23 +19,242 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type ContainerMemberStats struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + UncompressedSize uint64 `protobuf:"varint,2,opt,name=uncompressed_size,json=uncompressedSize,proto3" json:"uncompressed_size,omitempty"` + CompressedSize uint64 `protobuf:"varint,3,opt,name=compressed_size,json=compressedSize,proto3" json:"compressed_size,omitempty"` +} + +func (x *ContainerMemberStats) Reset() { + *x = ContainerMemberStats{} + if protoimpl.UnsafeEnabled { + mi := &file_flows_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ContainerMemberStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContainerMemberStats) ProtoMessage() {} + +func (x *ContainerMemberStats) ProtoReflect() protoreflect.Message { + mi := &file_flows_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContainerMemberStats.ProtoReflect.Descriptor instead. +func (*ContainerMemberStats) Descriptor() ([]byte, []int) { + return file_flows_proto_rawDescGZIP(), []int{0} +} + +func (x *ContainerMemberStats) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ContainerMemberStats) GetUncompressedSize() uint64 { + if x != nil { + return x.UncompressedSize + } + return 0 +} + +func (x *ContainerMemberStats) GetCompressedSize() uint64 { + if x != nil { + return x.CompressedSize + } + return 0 +} + +// Stats about exported containers +type ContainerStats struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Seconds since epoch + Timestamp uint64 `protobuf:"varint,8,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + TotalUploadedFiles uint64 `protobuf:"varint,1,opt,name=total_uploaded_files,json=totalUploadedFiles,proto3" json:"total_uploaded_files,omitempty"` + TotalUploadedBytes uint64 `protobuf:"varint,2,opt,name=total_uploaded_bytes,json=totalUploadedBytes,proto3" json:"total_uploaded_bytes,omitempty"` + // Total number of bytes written to the container. NOTE: Due to + // caching this number can increase substantially before the + // container size actually changes so this is a better measure of + // progress than the compressed size. + TotalUncompressedBytes uint64 `protobuf:"varint,3,opt,name=total_uncompressed_bytes,json=totalUncompressedBytes,proto3" json:"total_uncompressed_bytes,omitempty"` + TotalCompressedBytes uint64 `protobuf:"varint,4,opt,name=total_compressed_bytes,json=totalCompressedBytes,proto3" json:"total_compressed_bytes,omitempty"` + TotalContainerFiles uint64 `protobuf:"varint,5,opt,name=total_container_files,json=totalContainerFiles,proto3" json:"total_container_files,omitempty"` + // The hash of the written container - this is only populated + // **after** the container is closed. + Hash string `protobuf:"bytes,6,opt,name=hash,proto3" json:"hash,omitempty"` + // Total number of seconds taken to compress. + TotalDuration uint64 `protobuf:"varint,9,opt,name=total_duration,json=totalDuration,proto3" json:"total_duration,omitempty"` + // Where the file can be downloaded from the filestore + Components []string `protobuf:"bytes,7,rep,name=components,proto3" json:"components,omitempty"` + Type string `protobuf:"bytes,10,opt,name=type,proto3" json:"type,omitempty"` + Error string `protobuf:"bytes,11,opt,name=error,proto3" json:"error,omitempty"` + // A string representation of the file path + VfsPath string `protobuf:"bytes,12,opt,name=vfs_path,json=vfsPath,proto3" json:"vfs_path,omitempty"` + ActiveMembers []*ContainerMemberStats `protobuf:"bytes,13,rep,name=active_members,json=activeMembers,proto3" json:"active_members,omitempty"` +} + +func (x *ContainerStats) Reset() { + *x = ContainerStats{} + if protoimpl.UnsafeEnabled { + mi := &file_flows_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ContainerStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContainerStats) ProtoMessage() {} + +func (x *ContainerStats) ProtoReflect() protoreflect.Message { + mi := &file_flows_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContainerStats.ProtoReflect.Descriptor instead. +func (*ContainerStats) Descriptor() ([]byte, []int) { + return file_flows_proto_rawDescGZIP(), []int{1} +} + +func (x *ContainerStats) GetTimestamp() uint64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *ContainerStats) GetTotalUploadedFiles() uint64 { + if x != nil { + return x.TotalUploadedFiles + } + return 0 +} + +func (x *ContainerStats) GetTotalUploadedBytes() uint64 { + if x != nil { + return x.TotalUploadedBytes + } + return 0 +} + +func (x *ContainerStats) GetTotalUncompressedBytes() uint64 { + if x != nil { + return x.TotalUncompressedBytes + } + return 0 +} + +func (x *ContainerStats) GetTotalCompressedBytes() uint64 { + if x != nil { + return x.TotalCompressedBytes + } + return 0 +} + +func (x *ContainerStats) GetTotalContainerFiles() uint64 { + if x != nil { + return x.TotalContainerFiles + } + return 0 +} + +func (x *ContainerStats) GetHash() string { + if x != nil { + return x.Hash + } + return "" +} + +func (x *ContainerStats) GetTotalDuration() uint64 { + if x != nil { + return x.TotalDuration + } + return 0 +} + +func (x *ContainerStats) GetComponents() []string { + if x != nil { + return x.Components + } + return nil +} + +func (x *ContainerStats) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *ContainerStats) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *ContainerStats) GetVfsPath() string { + if x != nil { + return x.VfsPath + } + return "" +} + +func (x *ContainerStats) GetActiveMembers() []*ContainerMemberStats { + if x != nil { + return x.ActiveMembers + } + return nil +} + type AvailableDownloadFile struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` - Path string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"` - Complete bool `protobuf:"varint,2,opt,name=complete,proto3" json:"complete,omitempty"` - Size uint64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"` - Date string `protobuf:"bytes,4,opt,name=date,proto3" json:"date,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Path string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"` + // Deprecated things are now stored in the stats. + Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` + Complete bool `protobuf:"varint,2,opt,name=complete,proto3" json:"complete,omitempty"` + Size uint64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"` + Date string `protobuf:"bytes,4,opt,name=date,proto3" json:"date,omitempty"` + Stats *ContainerStats `protobuf:"bytes,8,opt,name=stats,proto3" json:"stats,omitempty"` } func (x *AvailableDownloadFile) Reset() { *x = AvailableDownloadFile{} if protoimpl.UnsafeEnabled { - mi := &file_flows_proto_msgTypes[0] + mi := &file_flows_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -51,7 +267,7 @@ func (x *AvailableDownloadFile) String() string { func (*AvailableDownloadFile) ProtoMessage() {} func (x *AvailableDownloadFile) ProtoReflect() protoreflect.Message { - mi := &file_flows_proto_msgTypes[0] + mi := &file_flows_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -64,7 +280,7 @@ func (x *AvailableDownloadFile) ProtoReflect() protoreflect.Message { // Deprecated: Use AvailableDownloadFile.ProtoReflect.Descriptor instead. func (*AvailableDownloadFile) Descriptor() ([]byte, []int) { - return file_flows_proto_rawDescGZIP(), []int{0} + return file_flows_proto_rawDescGZIP(), []int{2} } func (x *AvailableDownloadFile) GetName() string { @@ -74,16 +290,16 @@ func (x *AvailableDownloadFile) GetName() string { return "" } -func (x *AvailableDownloadFile) GetType() string { +func (x *AvailableDownloadFile) GetPath() string { if x != nil { - return x.Type + return x.Path } return "" } -func (x *AvailableDownloadFile) GetPath() string { +func (x *AvailableDownloadFile) GetType() string { if x != nil { - return x.Path + return x.Type } return "" } @@ -109,6 +325,13 @@ func (x *AvailableDownloadFile) GetDate() string { return "" } +func (x *AvailableDownloadFile) GetStats() *ContainerStats { + if x != nil { + return x.Stats + } + return nil +} + type AvailableDownloads struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -120,7 +343,7 @@ type AvailableDownloads struct { func (x *AvailableDownloads) Reset() { *x = AvailableDownloads{} if protoimpl.UnsafeEnabled { - mi := &file_flows_proto_msgTypes[1] + mi := &file_flows_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -133,7 +356,7 @@ func (x *AvailableDownloads) String() string { func (*AvailableDownloads) ProtoMessage() {} func (x *AvailableDownloads) ProtoReflect() protoreflect.Message { - mi := &file_flows_proto_msgTypes[1] + mi := &file_flows_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -146,7 +369,7 @@ func (x *AvailableDownloads) ProtoReflect() protoreflect.Message { // Deprecated: Use AvailableDownloads.ProtoReflect.Descriptor instead. func (*AvailableDownloads) Descriptor() ([]byte, []int) { - return file_flows_proto_rawDescGZIP(), []int{1} + return file_flows_proto_rawDescGZIP(), []int{3} } func (x *AvailableDownloads) GetFiles() []*AvailableDownloadFile { @@ -168,7 +391,7 @@ type FlowDetails struct { func (x *FlowDetails) Reset() { *x = FlowDetails{} if protoimpl.UnsafeEnabled { - mi := &file_flows_proto_msgTypes[2] + mi := &file_flows_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -181,7 +404,7 @@ func (x *FlowDetails) String() string { func (*FlowDetails) ProtoMessage() {} func (x *FlowDetails) ProtoReflect() protoreflect.Message { - mi := &file_flows_proto_msgTypes[2] + mi := &file_flows_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -194,7 +417,7 @@ func (x *FlowDetails) ProtoReflect() protoreflect.Message { // Deprecated: Use FlowDetails.ProtoReflect.Descriptor instead. func (*FlowDetails) Descriptor() ([]byte, []int) { - return file_flows_proto_rawDescGZIP(), []int{2} + return file_flows_proto_rawDescGZIP(), []int{4} } func (x *FlowDetails) GetContext() *proto.ArtifactCollectorContext { @@ -220,13 +443,15 @@ type ApiFlowRequestDetails struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Items []*proto1.VeloMessage `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + Items []*proto1.VeloMessage `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + FlowId string `protobuf:"bytes,3,opt,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` } func (x *ApiFlowRequestDetails) Reset() { *x = ApiFlowRequestDetails{} if protoimpl.UnsafeEnabled { - mi := &file_flows_proto_msgTypes[3] + mi := &file_flows_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -239,7 +464,7 @@ func (x *ApiFlowRequestDetails) String() string { func (*ApiFlowRequestDetails) ProtoMessage() {} func (x *ApiFlowRequestDetails) ProtoReflect() protoreflect.Message { - mi := &file_flows_proto_msgTypes[3] + mi := &file_flows_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -252,7 +477,7 @@ func (x *ApiFlowRequestDetails) ProtoReflect() protoreflect.Message { // Deprecated: Use ApiFlowRequestDetails.ProtoReflect.Descriptor instead. func (*ApiFlowRequestDetails) Descriptor() ([]byte, []int) { - return file_flows_proto_rawDescGZIP(), []int{3} + return file_flows_proto_rawDescGZIP(), []int{5} } func (x *ApiFlowRequestDetails) GetItems() []*proto1.VeloMessage { @@ -262,6 +487,20 @@ func (x *ApiFlowRequestDetails) GetItems() []*proto1.VeloMessage { return nil } +func (x *ApiFlowRequestDetails) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *ApiFlowRequestDetails) GetFlowId() string { + if x != nil { + return x.FlowId + } + return "" +} + type ApiFlowResultDetails struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -273,7 +512,7 @@ type ApiFlowResultDetails struct { func (x *ApiFlowResultDetails) Reset() { *x = ApiFlowResultDetails{} if protoimpl.UnsafeEnabled { - mi := &file_flows_proto_msgTypes[4] + mi := &file_flows_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -286,7 +525,7 @@ func (x *ApiFlowResultDetails) String() string { func (*ApiFlowResultDetails) ProtoMessage() {} func (x *ApiFlowResultDetails) ProtoReflect() protoreflect.Message { - mi := &file_flows_proto_msgTypes[4] + mi := &file_flows_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -299,7 +538,7 @@ func (x *ApiFlowResultDetails) ProtoReflect() protoreflect.Message { // Deprecated: Use ApiFlowResultDetails.ProtoReflect.Descriptor instead. func (*ApiFlowResultDetails) Descriptor() ([]byte, []int) { - return file_flows_proto_rawDescGZIP(), []int{4} + return file_flows_proto_rawDescGZIP(), []int{6} } func (x *ApiFlowResultDetails) GetItems() []*proto1.VeloMessage { @@ -320,7 +559,7 @@ type ApiFlowLogDetails struct { func (x *ApiFlowLogDetails) Reset() { *x = ApiFlowLogDetails{} if protoimpl.UnsafeEnabled { - mi := &file_flows_proto_msgTypes[5] + mi := &file_flows_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -333,7 +572,7 @@ func (x *ApiFlowLogDetails) String() string { func (*ApiFlowLogDetails) ProtoMessage() {} func (x *ApiFlowLogDetails) ProtoReflect() protoreflect.Message { - mi := &file_flows_proto_msgTypes[5] + mi := &file_flows_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -346,7 +585,7 @@ func (x *ApiFlowLogDetails) ProtoReflect() protoreflect.Message { // Deprecated: Use ApiFlowLogDetails.ProtoReflect.Descriptor instead. func (*ApiFlowLogDetails) Descriptor() ([]byte, []int) { - return file_flows_proto_rawDescGZIP(), []int{5} + return file_flows_proto_rawDescGZIP(), []int{7} } func (x *ApiFlowLogDetails) GetItems() []*proto1.LogMessage { @@ -373,7 +612,7 @@ type ApiFlowRequest struct { func (x *ApiFlowRequest) Reset() { *x = ApiFlowRequest{} if protoimpl.UnsafeEnabled { - mi := &file_flows_proto_msgTypes[6] + mi := &file_flows_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -386,7 +625,7 @@ func (x *ApiFlowRequest) String() string { func (*ApiFlowRequest) ProtoMessage() {} func (x *ApiFlowRequest) ProtoReflect() protoreflect.Message { - mi := &file_flows_proto_msgTypes[6] + mi := &file_flows_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -399,7 +638,7 @@ func (x *ApiFlowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApiFlowRequest.ProtoReflect.Descriptor instead. func (*ApiFlowRequest) Descriptor() ([]byte, []int) { - return file_flows_proto_rawDescGZIP(), []int{6} + return file_flows_proto_rawDescGZIP(), []int{8} } func (x *ApiFlowRequest) GetClientId() string { @@ -449,13 +688,14 @@ type ApiFlowResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"` Items []*proto.ArtifactCollectorContext `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` } func (x *ApiFlowResponse) Reset() { *x = ApiFlowResponse{} if protoimpl.UnsafeEnabled { - mi := &file_flows_proto_msgTypes[7] + mi := &file_flows_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -468,7 +708,7 @@ func (x *ApiFlowResponse) String() string { func (*ApiFlowResponse) ProtoMessage() {} func (x *ApiFlowResponse) ProtoReflect() protoreflect.Message { - mi := &file_flows_proto_msgTypes[7] + mi := &file_flows_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -481,7 +721,14 @@ func (x *ApiFlowResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApiFlowResponse.ProtoReflect.Descriptor instead. func (*ApiFlowResponse) Descriptor() ([]byte, []int) { - return file_flows_proto_rawDescGZIP(), []int{7} + return file_flows_proto_rawDescGZIP(), []int{9} +} + +func (x *ApiFlowResponse) GetTotal() uint64 { + if x != nil { + return x.Total + } + return 0 } func (x *ApiFlowResponse) GetItems() []*proto.ArtifactCollectorContext { @@ -499,63 +746,112 @@ var file_flows_proto_rawDesc = []byte{ 0x74, 0x6f, 0x2f, 0x6a, 0x6f, 0x62, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0x97, 0x01, 0x0a, 0x15, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, + 0x6f, 0x74, 0x6f, 0x22, 0x80, 0x01, 0x0a, 0x14, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x2b, 0x0a, 0x11, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, + 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x75, 0x6e, 0x63, + 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x27, 0x0a, + 0x0f, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x73, 0x69, 0x7a, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, + 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x9a, 0x04, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x30, 0x0a, 0x14, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x5f, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x55, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x5f, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x55, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x38, 0x0a, 0x18, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, + 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x16, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x55, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, + 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, + 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x14, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x6d, 0x70, + 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x13, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, + 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, + 0x61, 0x73, 0x68, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x64, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, + 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x12, 0x19, 0x0a, 0x08, 0x76, 0x66, 0x73, 0x5f, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x66, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, + 0x42, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x53, + 0x74, 0x61, 0x74, 0x73, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4d, 0x65, 0x6d, 0x62, + 0x65, 0x72, 0x73, 0x22, 0xc4, 0x01, 0x0a, 0x15, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6d, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x22, 0x48, 0x0a, - 0x12, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, - 0x61, 0x64, 0x73, 0x12, 0x32, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x76, 0x61, 0x69, 0x6c, - 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, - 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x94, 0x01, 0x0a, 0x0b, 0x46, 0x6c, 0x6f, 0x77, - 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x39, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x78, 0x74, 0x12, 0x4a, 0x0a, 0x13, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, - 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, - 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x52, 0x12, 0x61, 0x76, 0x61, 0x69, - 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x22, 0x41, - 0x0a, 0x15, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x28, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, - 0x65, 0x6c, 0x6f, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, - 0x73, 0x22, 0x40, 0x0a, 0x14, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x28, 0x0a, 0x05, 0x69, 0x74, 0x65, - 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x56, 0x65, 0x6c, 0x6f, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x05, 0x69, 0x74, - 0x65, 0x6d, 0x73, 0x22, 0x3c, 0x0a, 0x11, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x4c, 0x6f, - 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x4c, 0x6f, 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, - 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x0e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, - 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, - 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, - 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, - 0x75, 0x64, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, - 0x76, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, - 0x48, 0x0a, 0x0f, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, - 0x78, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, - 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, - 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, - 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x12, 0x2b, 0x0a, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x48, 0x0a, 0x12, 0x41, 0x76, + 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, + 0x12, 0x32, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, + 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x22, 0x94, 0x01, 0x0a, 0x0b, 0x46, 0x6c, 0x6f, 0x77, 0x44, 0x65, 0x74, + 0x61, 0x69, 0x6c, 0x73, 0x12, 0x39, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x43, + 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, + 0x4a, 0x0a, 0x13, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6f, 0x77, + 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, + 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x52, 0x12, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, + 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x22, 0x77, 0x0a, 0x15, 0x41, + 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x74, + 0x61, 0x69, 0x6c, 0x73, 0x12, 0x28, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x6c, 0x6f, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x1b, + 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, + 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, + 0x6f, 0x77, 0x49, 0x64, 0x22, 0x40, 0x0a, 0x14, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x28, 0x0a, 0x05, + 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x6c, 0x6f, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x3c, 0x0a, 0x11, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, + 0x77, 0x4c, 0x6f, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x0e, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x16, 0x0a, + 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, + 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x69, + 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x41, 0x72, + 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x22, 0x5e, 0x0a, 0x0f, 0x41, 0x70, 0x69, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x35, 0x0a, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, + 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, + 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -570,33 +866,37 @@ func file_flows_proto_rawDescGZIP() []byte { return file_flows_proto_rawDescData } -var file_flows_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_flows_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_flows_proto_goTypes = []interface{}{ - (*AvailableDownloadFile)(nil), // 0: proto.AvailableDownloadFile - (*AvailableDownloads)(nil), // 1: proto.AvailableDownloads - (*FlowDetails)(nil), // 2: proto.FlowDetails - (*ApiFlowRequestDetails)(nil), // 3: proto.ApiFlowRequestDetails - (*ApiFlowResultDetails)(nil), // 4: proto.ApiFlowResultDetails - (*ApiFlowLogDetails)(nil), // 5: proto.ApiFlowLogDetails - (*ApiFlowRequest)(nil), // 6: proto.ApiFlowRequest - (*ApiFlowResponse)(nil), // 7: proto.ApiFlowResponse - (*proto.ArtifactCollectorContext)(nil), // 8: proto.ArtifactCollectorContext - (*proto1.VeloMessage)(nil), // 9: proto.VeloMessage - (*proto1.LogMessage)(nil), // 10: proto.LogMessage + (*ContainerMemberStats)(nil), // 0: proto.ContainerMemberStats + (*ContainerStats)(nil), // 1: proto.ContainerStats + (*AvailableDownloadFile)(nil), // 2: proto.AvailableDownloadFile + (*AvailableDownloads)(nil), // 3: proto.AvailableDownloads + (*FlowDetails)(nil), // 4: proto.FlowDetails + (*ApiFlowRequestDetails)(nil), // 5: proto.ApiFlowRequestDetails + (*ApiFlowResultDetails)(nil), // 6: proto.ApiFlowResultDetails + (*ApiFlowLogDetails)(nil), // 7: proto.ApiFlowLogDetails + (*ApiFlowRequest)(nil), // 8: proto.ApiFlowRequest + (*ApiFlowResponse)(nil), // 9: proto.ApiFlowResponse + (*proto.ArtifactCollectorContext)(nil), // 10: proto.ArtifactCollectorContext + (*proto1.VeloMessage)(nil), // 11: proto.VeloMessage + (*proto1.LogMessage)(nil), // 12: proto.LogMessage } var file_flows_proto_depIdxs = []int32{ - 0, // 0: proto.AvailableDownloads.files:type_name -> proto.AvailableDownloadFile - 8, // 1: proto.FlowDetails.context:type_name -> proto.ArtifactCollectorContext - 1, // 2: proto.FlowDetails.available_downloads:type_name -> proto.AvailableDownloads - 9, // 3: proto.ApiFlowRequestDetails.items:type_name -> proto.VeloMessage - 9, // 4: proto.ApiFlowResultDetails.items:type_name -> proto.VeloMessage - 10, // 5: proto.ApiFlowLogDetails.items:type_name -> proto.LogMessage - 8, // 6: proto.ApiFlowResponse.items:type_name -> proto.ArtifactCollectorContext - 7, // [7:7] is the sub-list for method output_type - 7, // [7:7] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 0, // 0: proto.ContainerStats.active_members:type_name -> proto.ContainerMemberStats + 1, // 1: proto.AvailableDownloadFile.stats:type_name -> proto.ContainerStats + 2, // 2: proto.AvailableDownloads.files:type_name -> proto.AvailableDownloadFile + 10, // 3: proto.FlowDetails.context:type_name -> proto.ArtifactCollectorContext + 3, // 4: proto.FlowDetails.available_downloads:type_name -> proto.AvailableDownloads + 11, // 5: proto.ApiFlowRequestDetails.items:type_name -> proto.VeloMessage + 11, // 6: proto.ApiFlowResultDetails.items:type_name -> proto.VeloMessage + 12, // 7: proto.ApiFlowLogDetails.items:type_name -> proto.LogMessage + 10, // 8: proto.ApiFlowResponse.items:type_name -> proto.ArtifactCollectorContext + 9, // [9:9] is the sub-list for method output_type + 9, // [9:9] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_flows_proto_init() } @@ -606,7 +906,7 @@ func file_flows_proto_init() { } if !protoimpl.UnsafeEnabled { file_flows_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AvailableDownloadFile); i { + switch v := v.(*ContainerMemberStats); i { case 0: return &v.state case 1: @@ -618,7 +918,7 @@ func file_flows_proto_init() { } } file_flows_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AvailableDownloads); i { + switch v := v.(*ContainerStats); i { case 0: return &v.state case 1: @@ -630,7 +930,7 @@ func file_flows_proto_init() { } } file_flows_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FlowDetails); i { + switch v := v.(*AvailableDownloadFile); i { case 0: return &v.state case 1: @@ -642,7 +942,7 @@ func file_flows_proto_init() { } } file_flows_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApiFlowRequestDetails); i { + switch v := v.(*AvailableDownloads); i { case 0: return &v.state case 1: @@ -654,7 +954,7 @@ func file_flows_proto_init() { } } file_flows_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApiFlowResultDetails); i { + switch v := v.(*FlowDetails); i { case 0: return &v.state case 1: @@ -666,7 +966,7 @@ func file_flows_proto_init() { } } file_flows_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApiFlowLogDetails); i { + switch v := v.(*ApiFlowRequestDetails); i { case 0: return &v.state case 1: @@ -678,7 +978,7 @@ func file_flows_proto_init() { } } file_flows_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApiFlowRequest); i { + switch v := v.(*ApiFlowResultDetails); i { case 0: return &v.state case 1: @@ -690,6 +990,30 @@ func file_flows_proto_init() { } } file_flows_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ApiFlowLogDetails); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_flows_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ApiFlowRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_flows_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ApiFlowResponse); i { case 0: return &v.state @@ -708,7 +1032,7 @@ func file_flows_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_flows_proto_rawDesc, NumEnums: 0, - NumMessages: 8, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/api/proto/flows.proto b/api/proto/flows.proto index 1f9095841..fdc231f29 100644 --- a/api/proto/flows.proto +++ b/api/proto/flows.proto @@ -7,13 +7,60 @@ package proto; option go_package = "www.velocidex.com/golang/velociraptor/api/proto"; +message ContainerMemberStats { + string name = 1; + uint64 uncompressed_size = 2; + uint64 compressed_size = 3; +} + +// Stats about exported containers +message ContainerStats { + // Seconds since epoch + uint64 timestamp = 8; + + uint64 total_uploaded_files = 1; + uint64 total_uploaded_bytes = 2; + + // Total number of bytes written to the container. NOTE: Due to + // caching this number can increase substantially before the + // container size actually changes so this is a better measure of + // progress than the compressed size. + uint64 total_uncompressed_bytes = 3; + uint64 total_compressed_bytes = 4; + uint64 total_container_files = 5; + + // The hash of the written container - this is only populated + // **after** the container is closed. + string hash = 6; + + // Total number of seconds taken to compress. + uint64 total_duration = 9; + + // Where the file can be downloaded from the filestore + repeated string components = 7; + + string type = 10; + + string error = 11; + + // A string representation of the file path + string vfs_path = 12; + + repeated ContainerMemberStats active_members = 13; + +} + message AvailableDownloadFile { string name = 1; - string type = 6; string path = 5; + + // Deprecated things are now stored in the stats. + string type = 6; bool complete = 2; uint64 size = 3; string date = 4; + + ContainerStats stats = 8; } message AvailableDownloads { @@ -31,6 +78,8 @@ message FlowDetails { // artifacts - they only interprect raw VQL as compiled by the server. message ApiFlowRequestDetails { repeated VeloMessage items = 1; + string client_id = 2; + string flow_id = 3; } message ApiFlowResultDetails { @@ -53,5 +102,6 @@ message ApiFlowRequest { } message ApiFlowResponse { + uint64 total = 1; repeated ArtifactCollectorContext items = 2; } diff --git a/api/proto/health.pb.go b/api/proto/health.pb.go index f01d2d234..de1eedaff 100644 --- a/api/proto/health.pb.go +++ b/api/proto/health.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: health.proto package proto diff --git a/api/proto/hunts.pb.go b/api/proto/hunts.pb.go index b2a1dc9b9..bf29d8303 100644 --- a/api/proto/hunts.pb.go +++ b/api/proto/hunts.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: hunts.proto package proto @@ -82,6 +79,8 @@ const ( Hunt_RUNNING Hunt_State = 2 Hunt_STOPPED Hunt_State = 3 Hunt_ARCHIVED Hunt_State = 4 + // Set internally when the hunt is being deleted. + Hunt_DELETED Hunt_State = 5 ) // Enum value maps for Hunt_State. @@ -92,6 +91,7 @@ var ( 2: "RUNNING", 3: "STOPPED", 4: "ARCHIVED", + 5: "DELETED", } Hunt_State_value = map[string]int32{ "UNSET": 0, @@ -99,6 +99,7 @@ var ( "RUNNING": 2, "STOPPED": 3, "ARCHIVED": 4, + "DELETED": 5, } ) @@ -230,6 +231,7 @@ type HuntCondition struct { ExcludedLabels *HuntLabelCondition `protobuf:"bytes,4,opt,name=excluded_labels,json=excludedLabels,proto3" json:"excluded_labels,omitempty"` // Types that are assignable to UnionField: + // // *HuntCondition_Labels // *HuntCondition_Os UnionField isHuntCondition_UnionField `protobuf_oneof:"union_field"` @@ -320,8 +322,15 @@ type HuntStats struct { TotalClientsWithResults uint64 `protobuf:"varint,14,opt,name=total_clients_with_results,json=totalClientsWithResults,proto3" json:"total_clients_with_results,omitempty"` TotalClientsWithoutResults uint64 `protobuf:"varint,16,opt,name=total_clients_without_results,json=totalClientsWithoutResults,proto3" json:"total_clients_without_results,omitempty"` TotalClientsWithErrors uint64 `protobuf:"varint,15,opt,name=total_clients_with_errors,json=totalClientsWithErrors,proto3" json:"total_clients_with_errors,omitempty"` + TotalUploadedBytes uint64 `protobuf:"varint,18,opt,name=total_uploaded_bytes,json=totalUploadedBytes,proto3" json:"total_uploaded_bytes,omitempty"` + TotalCollectedRows uint64 `protobuf:"varint,19,opt,name=total_collected_rows,json=totalCollectedRows,proto3" json:"total_collected_rows,omitempty"` + TotalFinishedClients uint64 `protobuf:"varint,20,opt,name=total_finished_clients,json=totalFinishedClients,proto3" json:"total_finished_clients,omitempty"` Stopped bool `protobuf:"varint,1,opt,name=stopped,proto3" json:"stopped,omitempty"` AvailableDownloads *AvailableDownloads `protobuf:"bytes,2,opt,name=available_downloads,json=availableDownloads,proto3" json:"available_downloads,omitempty"` + // The last time the client table was scanned. This is used + // internally for a quick comparison when refreshing the hunt + // stats. + LastClientTableScan int64 `protobuf:"varint,17,opt,name=last_client_table_scan,json=lastClientTableScan,proto3" json:"last_client_table_scan,omitempty"` } func (x *HuntStats) Reset() { @@ -384,6 +393,27 @@ func (x *HuntStats) GetTotalClientsWithErrors() uint64 { return 0 } +func (x *HuntStats) GetTotalUploadedBytes() uint64 { + if x != nil { + return x.TotalUploadedBytes + } + return 0 +} + +func (x *HuntStats) GetTotalCollectedRows() uint64 { + if x != nil { + return x.TotalCollectedRows + } + return 0 +} + +func (x *HuntStats) GetTotalFinishedClients() uint64 { + if x != nil { + return x.TotalFinishedClients + } + return 0 +} + func (x *HuntStats) GetStopped() bool { if x != nil { return x.Stopped @@ -398,18 +428,27 @@ func (x *HuntStats) GetAvailableDownloads() *AvailableDownloads { return nil } +func (x *HuntStats) GetLastClientTableScan() int64 { + if x != nil { + return x.LastClientTableScan + } + return 0 +} + type Hunt struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - HuntId string `protobuf:"bytes,1,opt,name=hunt_id,json=huntId,proto3" json:"hunt_id,omitempty"` - Version int64 `protobuf:"varint,20,opt,name=version,proto3" json:"version,omitempty"` - CreateTime uint64 `protobuf:"varint,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` - Creator string `protobuf:"bytes,12,opt,name=creator,proto3" json:"creator,omitempty"` - StartTime uint64 `protobuf:"varint,21,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` - Expires uint64 `protobuf:"varint,10,opt,name=expires,proto3" json:"expires,omitempty"` - HuntDescription string `protobuf:"bytes,11,opt,name=hunt_description,json=huntDescription,proto3" json:"hunt_description,omitempty"` + HuntId string `protobuf:"bytes,1,opt,name=hunt_id,json=huntId,proto3" json:"hunt_id,omitempty"` + Version int64 `protobuf:"varint,20,opt,name=version,proto3" json:"version,omitempty"` + CreateTime uint64 `protobuf:"varint,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + Creator string `protobuf:"bytes,12,opt,name=creator,proto3" json:"creator,omitempty"` + StartTime uint64 `protobuf:"varint,21,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + Expires uint64 `protobuf:"varint,10,opt,name=expires,proto3" json:"expires,omitempty"` + HuntDescription string `protobuf:"bytes,11,opt,name=hunt_description,json=huntDescription,proto3" json:"hunt_description,omitempty"` + // Hunts can be tagged. + Tags []string `protobuf:"bytes,23,rep,name=tags,proto3" json:"tags,omitempty"` StartRequest *proto.ArtifactCollectorArgs `protobuf:"bytes,16,opt,name=start_request,json=startRequest,proto3" json:"start_request,omitempty"` Condition *HuntCondition `protobuf:"bytes,4,opt,name=condition,proto3" json:"condition,omitempty"` ClientLimit uint64 `protobuf:"varint,6,opt,name=client_limit,json=clientLimit,proto3" json:"client_limit,omitempty"` @@ -417,6 +456,8 @@ type Hunt struct { Artifacts []string `protobuf:"bytes,17,rep,name=artifacts,proto3" json:"artifacts,omitempty"` ArtifactSources []string `protobuf:"bytes,19,rep,name=artifact_sources,json=artifactSources,proto3" json:"artifact_sources,omitempty"` State Hunt_State `protobuf:"varint,8,opt,name=state,proto3,enum=proto.Hunt_State" json:"state,omitempty"` + // A list of the org IDs that the hunt will be launched on + OrgIds []string `protobuf:"bytes,22,rep,name=org_ids,json=orgIds,proto3" json:"org_ids,omitempty"` } func (x *Hunt) Reset() { @@ -500,6 +541,13 @@ func (x *Hunt) GetHuntDescription() string { return "" } +func (x *Hunt) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + func (x *Hunt) GetStartRequest() *proto.ArtifactCollectorArgs { if x != nil { return x.StartRequest @@ -549,6 +597,69 @@ func (x *Hunt) GetState() Hunt_State { return Hunt_UNSET } +func (x *Hunt) GetOrgIds() []string { + if x != nil { + return x.OrgIds + } + return nil +} + +type HuntEstimateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Only show clients that were active this many seconds ago. + LastActive uint64 `protobuf:"varint,1,opt,name=last_active,json=lastActive,proto3" json:"last_active,omitempty"` + Condition *HuntCondition `protobuf:"bytes,4,opt,name=condition,proto3" json:"condition,omitempty"` +} + +func (x *HuntEstimateRequest) Reset() { + *x = HuntEstimateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_hunts_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HuntEstimateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HuntEstimateRequest) ProtoMessage() {} + +func (x *HuntEstimateRequest) ProtoReflect() protoreflect.Message { + mi := &file_hunts_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HuntEstimateRequest.ProtoReflect.Descriptor instead. +func (*HuntEstimateRequest) Descriptor() ([]byte, []int) { + return file_hunts_proto_rawDescGZIP(), []int{5} +} + +func (x *HuntEstimateRequest) GetLastActive() uint64 { + if x != nil { + return x.LastActive + } + return 0 +} + +func (x *HuntEstimateRequest) GetCondition() *HuntCondition { + if x != nil { + return x.Condition + } + return nil +} + type ListHuntsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -557,14 +668,15 @@ type ListHuntsRequest struct { Offset uint64 `protobuf:"varint,1,opt,name=offset,proto3" json:"offset,omitempty"` Count uint64 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` // If specified we return a partial structure. - Summary bool `protobuf:"varint,4,opt,name=summary,proto3" json:"summary,omitempty"` - IncludeArchived bool `protobuf:"varint,3,opt,name=include_archived,json=includeArchived,proto3" json:"include_archived,omitempty"` + Summary bool `protobuf:"varint,4,opt,name=summary,proto3" json:"summary,omitempty"` + IncludeArchived bool `protobuf:"varint,3,opt,name=include_archived,json=includeArchived,proto3" json:"include_archived,omitempty"` + UserFilter string `protobuf:"bytes,5,opt,name=user_filter,json=userFilter,proto3" json:"user_filter,omitempty"` } func (x *ListHuntsRequest) Reset() { *x = ListHuntsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_hunts_proto_msgTypes[5] + mi := &file_hunts_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -577,7 +689,7 @@ func (x *ListHuntsRequest) String() string { func (*ListHuntsRequest) ProtoMessage() {} func (x *ListHuntsRequest) ProtoReflect() protoreflect.Message { - mi := &file_hunts_proto_msgTypes[5] + mi := &file_hunts_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -590,7 +702,7 @@ func (x *ListHuntsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListHuntsRequest.ProtoReflect.Descriptor instead. func (*ListHuntsRequest) Descriptor() ([]byte, []int) { - return file_hunts_proto_rawDescGZIP(), []int{5} + return file_hunts_proto_rawDescGZIP(), []int{6} } func (x *ListHuntsRequest) GetOffset() uint64 { @@ -621,18 +733,26 @@ func (x *ListHuntsRequest) GetIncludeArchived() bool { return false } +func (x *ListHuntsRequest) GetUserFilter() string { + if x != nil { + return x.UserFilter + } + return "" +} + type ListHuntsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` Items []*Hunt `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` } func (x *ListHuntsResponse) Reset() { *x = ListHuntsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_hunts_proto_msgTypes[6] + mi := &file_hunts_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -645,7 +765,7 @@ func (x *ListHuntsResponse) String() string { func (*ListHuntsResponse) ProtoMessage() {} func (x *ListHuntsResponse) ProtoReflect() protoreflect.Message { - mi := &file_hunts_proto_msgTypes[6] + mi := &file_hunts_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -658,7 +778,14 @@ func (x *ListHuntsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListHuntsResponse.ProtoReflect.Descriptor instead. func (*ListHuntsResponse) Descriptor() ([]byte, []int) { - return file_hunts_proto_rawDescGZIP(), []int{6} + return file_hunts_proto_rawDescGZIP(), []int{7} +} + +func (x *ListHuntsResponse) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 } func (x *ListHuntsResponse) GetItems() []*Hunt { @@ -673,13 +800,14 @@ type GetHuntRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - HuntId string `protobuf:"bytes,1,opt,name=hunt_id,json=huntId,proto3" json:"hunt_id,omitempty"` + HuntId string `protobuf:"bytes,1,opt,name=hunt_id,json=huntId,proto3" json:"hunt_id,omitempty"` + IncludeRequest bool `protobuf:"varint,2,opt,name=include_request,json=includeRequest,proto3" json:"include_request,omitempty"` } func (x *GetHuntRequest) Reset() { *x = GetHuntRequest{} if protoimpl.UnsafeEnabled { - mi := &file_hunts_proto_msgTypes[7] + mi := &file_hunts_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -692,7 +820,7 @@ func (x *GetHuntRequest) String() string { func (*GetHuntRequest) ProtoMessage() {} func (x *GetHuntRequest) ProtoReflect() protoreflect.Message { - mi := &file_hunts_proto_msgTypes[7] + mi := &file_hunts_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -705,7 +833,7 @@ func (x *GetHuntRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetHuntRequest.ProtoReflect.Descriptor instead. func (*GetHuntRequest) Descriptor() ([]byte, []int) { - return file_hunts_proto_rawDescGZIP(), []int{7} + return file_hunts_proto_rawDescGZIP(), []int{8} } func (x *GetHuntRequest) GetHuntId() string { @@ -715,6 +843,13 @@ func (x *GetHuntRequest) GetHuntId() string { return "" } +func (x *GetHuntRequest) GetIncludeRequest() bool { + if x != nil { + return x.IncludeRequest + } + return false +} + type GetHuntResultsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -729,7 +864,7 @@ type GetHuntResultsRequest struct { func (x *GetHuntResultsRequest) Reset() { *x = GetHuntResultsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_hunts_proto_msgTypes[8] + mi := &file_hunts_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -742,7 +877,7 @@ func (x *GetHuntResultsRequest) String() string { func (*GetHuntResultsRequest) ProtoMessage() {} func (x *GetHuntResultsRequest) ProtoReflect() protoreflect.Message { - mi := &file_hunts_proto_msgTypes[8] + mi := &file_hunts_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -755,7 +890,7 @@ func (x *GetHuntResultsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetHuntResultsRequest.ProtoReflect.Descriptor instead. func (*GetHuntResultsRequest) Descriptor() ([]byte, []int) { - return file_hunts_proto_rawDescGZIP(), []int{8} + return file_hunts_proto_rawDescGZIP(), []int{9} } func (x *GetHuntResultsRequest) GetOffset() uint64 { @@ -798,7 +933,7 @@ type FlowAssignment struct { func (x *FlowAssignment) Reset() { *x = FlowAssignment{} if protoimpl.UnsafeEnabled { - mi := &file_hunts_proto_msgTypes[9] + mi := &file_hunts_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -811,7 +946,7 @@ func (x *FlowAssignment) String() string { func (*FlowAssignment) ProtoMessage() {} func (x *FlowAssignment) ProtoReflect() protoreflect.Message { - mi := &file_hunts_proto_msgTypes[9] + mi := &file_hunts_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -824,7 +959,7 @@ func (x *FlowAssignment) ProtoReflect() protoreflect.Message { // Deprecated: Use FlowAssignment.ProtoReflect.Descriptor instead. func (*FlowAssignment) Descriptor() ([]byte, []int) { - return file_hunts_proto_rawDescGZIP(), []int{9} + return file_hunts_proto_rawDescGZIP(), []int{10} } func (x *FlowAssignment) GetClientId() string { @@ -851,16 +986,20 @@ type HuntMutation struct { Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` State Hunt_State `protobuf:"varint,4,opt,name=state,proto3,enum=proto.Hunt_State" json:"state,omitempty"` StartTime uint64 `protobuf:"varint,5,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + Expires uint64 `protobuf:"varint,7,opt,name=expires,proto3" json:"expires,omitempty"` // A mutation can directly assign an existing flow to the // hunt. This allows a flow to be rerun and added to the hunt // later. Assignment *FlowAssignment `protobuf:"bytes,6,opt,name=assignment,proto3" json:"assignment,omitempty"` + Tags []string `protobuf:"bytes,8,rep,name=tags,proto3" json:"tags,omitempty"` + // The user who is initiating the mutation. + User string `protobuf:"bytes,9,opt,name=user,proto3" json:"user,omitempty"` } func (x *HuntMutation) Reset() { *x = HuntMutation{} if protoimpl.UnsafeEnabled { - mi := &file_hunts_proto_msgTypes[10] + mi := &file_hunts_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -873,7 +1012,7 @@ func (x *HuntMutation) String() string { func (*HuntMutation) ProtoMessage() {} func (x *HuntMutation) ProtoReflect() protoreflect.Message { - mi := &file_hunts_proto_msgTypes[10] + mi := &file_hunts_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -886,7 +1025,7 @@ func (x *HuntMutation) ProtoReflect() protoreflect.Message { // Deprecated: Use HuntMutation.ProtoReflect.Descriptor instead. func (*HuntMutation) Descriptor() ([]byte, []int) { - return file_hunts_proto_rawDescGZIP(), []int{10} + return file_hunts_proto_rawDescGZIP(), []int{11} } func (x *HuntMutation) GetHuntId() string { @@ -924,6 +1063,13 @@ func (x *HuntMutation) GetStartTime() uint64 { return 0 } +func (x *HuntMutation) GetExpires() uint64 { + if x != nil { + return x.Expires + } + return 0 +} + func (x *HuntMutation) GetAssignment() *FlowAssignment { if x != nil { return x.Assignment @@ -931,6 +1077,67 @@ func (x *HuntMutation) GetAssignment() *FlowAssignment { return nil } +func (x *HuntMutation) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *HuntMutation) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +type HuntTags struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tags []string `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty"` +} + +func (x *HuntTags) Reset() { + *x = HuntTags{} + if protoimpl.UnsafeEnabled { + mi := &file_hunts_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HuntTags) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HuntTags) ProtoMessage() {} + +func (x *HuntTags) ProtoReflect() protoreflect.Message { + mi := &file_hunts_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HuntTags.ProtoReflect.Descriptor instead. +func (*HuntTags) Descriptor() ([]byte, []int) { + return file_hunts_proto_rawDescGZIP(), []int{12} +} + +func (x *HuntTags) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + var File_hunts_proto protoreflect.FileDescriptor var file_hunts_proto_rawDesc = []byte{ @@ -968,7 +1175,7 @@ var file_hunts_proto_rawDesc = []byte{ 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x6f, 0x20, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x20, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x2e, 0x42, 0x0d, 0x0a, 0x0b, 0x75, 0x6e, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x69, 0x65, - 0x6c, 0x64, 0x22, 0x88, 0x06, 0x0a, 0x09, 0x48, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, + 0x6c, 0x64, 0x22, 0xd7, 0x07, 0x0a, 0x09, 0x48, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x17, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x42, 0x57, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x51, 0x12, 0x3e, 0x54, 0x68, 0x65, 0x20, @@ -1004,154 +1211,190 @@ var file_hunts_proto_rawDesc = []byte{ 0x72, 0x73, 0x2e, 0x22, 0x19, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x20, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x52, 0x16, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x57, 0x69, 0x74, 0x68, - 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x12, 0x79, 0x0a, 0x07, 0x73, 0x74, 0x6f, 0x70, 0x70, 0x65, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x5f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x59, 0x12, - 0x57, 0x49, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x73, 0x65, 0x74, 0x20, - 0x74, 0x68, 0x65, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x69, 0x73, - 0x20, 0x73, 0x74, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x2e, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x20, 0x69, 0x73, 0x20, 0x6d, 0x61, 0x6e, 0x69, 0x70, 0x75, 0x6c, 0x61, - 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x52, 0x07, 0x73, 0x74, 0x6f, 0x70, 0x70, 0x65, - 0x64, 0x12, 0x4a, 0x0a, 0x13, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, - 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, - 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x52, 0x12, 0x61, 0x76, 0x61, 0x69, 0x6c, - 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x22, 0x90, 0x0b, - 0x0a, 0x04, 0x48, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x09, 0x22, - 0x07, 0x48, 0x75, 0x6e, 0x74, 0x20, 0x49, 0x44, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, - 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x60, 0x0a, 0x0b, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, - 0x3f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x39, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, - 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x57, 0x68, 0x65, 0x6e, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, - 0x68, 0x75, 0x6e, 0x74, 0x20, 0x77, 0x61, 0x73, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x2e, 0x22, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x54, 0x69, 0x6d, 0x65, - 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x07, - 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1d, 0xe2, - 0xfc, 0xe3, 0xc4, 0x01, 0x17, 0x12, 0x15, 0x57, 0x68, 0x6f, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x3f, 0x52, 0x07, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x64, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x15, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, - 0x3f, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x24, - 0x57, 0x68, 0x65, 0x6e, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x77, - 0x61, 0x73, 0x20, 0x61, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x6c, 0x79, 0x20, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x65, 0x64, 0x2e, 0x22, 0x0a, 0x53, 0x74, 0x61, 0x72, 0x74, 0x20, 0x54, 0x69, 0x6d, 0x65, - 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x57, 0x0a, 0x07, 0x65, - 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x42, 0x3d, 0xe2, 0xfc, - 0xe3, 0xc4, 0x01, 0x37, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, - 0x65, 0x12, 0x1b, 0x57, 0x68, 0x65, 0x6e, 0x20, 0x64, 0x6f, 0x65, 0x73, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x3f, 0x22, 0x0b, - 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x20, 0x54, 0x69, 0x6d, 0x65, 0x52, 0x07, 0x65, 0x78, 0x70, - 0x69, 0x72, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x10, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1a, - 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x14, 0x12, 0x12, 0x48, 0x75, 0x6e, 0x74, 0x27, 0x73, 0x20, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x68, 0x75, 0x6e, 0x74, - 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x88, 0x01, 0x0a, 0x0d, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x10, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, - 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, 0x72, 0x67, - 0x73, 0x42, 0x45, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x3f, 0x12, 0x3d, 0x4c, 0x61, 0x75, 0x6e, 0x63, - 0x68, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, - 0x69, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x20, 0x69, 0x73, 0x20, 0x74, 0x72, 0x75, 0x65, 0x52, 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x8e, 0x01, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x42, 0x5a, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x54, 0x12, 0x42, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x6d, 0x75, 0x73, - 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, 0x64, 0x20, 0x66, - 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x62, - 0x65, 0x20, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x64, 0x2e, 0x22, 0x0e, 0x48, 0x75, - 0x6e, 0x74, 0x20, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x59, 0x0a, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x42, 0x36, 0xe2, - 0xfc, 0xe3, 0xc4, 0x01, 0x30, 0x12, 0x2e, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x20, 0x6e, 0x75, 0x6d, - 0x62, 0x65, 0x72, 0x20, 0x6f, 0x66, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x20, 0x74, - 0x68, 0x69, 0x73, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x72, 0x75, - 0x6e, 0x20, 0x6f, 0x6e, 0x2e, 0x52, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x6d, - 0x69, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x12, 0x4d, 0x0a, 0x09, 0x61, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x42, 0x2f, 0xe2, - 0xfc, 0xe3, 0xc4, 0x01, 0x29, 0x12, 0x27, 0x41, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, - 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, - 0x68, 0x75, 0x6e, 0x74, 0x20, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x65, 0x73, 0x2e, 0x52, 0x09, - 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x12, 0x61, 0x0a, 0x10, 0x61, 0x72, 0x74, - 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x13, 0x20, - 0x03, 0x28, 0x09, 0x42, 0x36, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x30, 0x12, 0x2e, 0x41, 0x20, 0x6c, - 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x20, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x68, 0x75, 0x6e, - 0x74, 0x20, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x65, 0x73, 0x2e, 0x52, 0x0f, 0x61, 0x72, 0x74, - 0x69, 0x66, 0x61, 0x63, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x71, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x42, 0x48, - 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x42, 0x12, 0x40, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x75, 0x6e, - 0x74, 0x2e, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x69, 0x73, - 0x20, 0x6d, 0x61, 0x6e, 0x75, 0x70, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x47, 0x55, 0x49, 0x2e, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, - 0xde, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x55, 0x4e, 0x53, - 0x45, 0x54, 0x10, 0x00, 0x12, 0x48, 0x0a, 0x06, 0x50, 0x41, 0x55, 0x53, 0x45, 0x44, 0x10, 0x01, - 0x1a, 0x3c, 0xea, 0xb9, 0xcb, 0xb9, 0x01, 0x36, 0x48, 0x75, 0x6e, 0x74, 0x20, 0x77, 0x69, 0x6c, - 0x6c, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x20, 0x6e, - 0x65, 0x77, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x63, - 0x61, 0x6e, 0x20, 0x62, 0x65, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x2e, 0x12, 0x2d, - 0x0a, 0x07, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x1a, 0x20, 0xea, 0xb9, 0xcb, - 0xb9, 0x01, 0x1a, 0x48, 0x75, 0x6e, 0x74, 0x20, 0x69, 0x73, 0x20, 0x72, 0x75, 0x6e, 0x6e, 0x69, - 0x6e, 0x67, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x72, 0x65, 0x61, 0x64, 0x79, 0x2e, 0x12, 0x24, 0x0a, - 0x07, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x03, 0x1a, 0x17, 0xea, 0xb9, 0xcb, 0xb9, - 0x01, 0x11, 0x48, 0x75, 0x6e, 0x74, 0x20, 0x68, 0x61, 0x73, 0x20, 0x73, 0x74, 0x6f, 0x70, 0x70, - 0x65, 0x64, 0x2e, 0x12, 0x2b, 0x0a, 0x08, 0x41, 0x52, 0x43, 0x48, 0x49, 0x56, 0x45, 0x44, 0x10, - 0x04, 0x1a, 0x1d, 0xea, 0xb9, 0xcb, 0xb9, 0x01, 0x17, 0x48, 0x75, 0x6e, 0x74, 0x20, 0x68, 0x61, - 0x73, 0x20, 0x62, 0x65, 0x65, 0x6e, 0x20, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x2e, - 0x22, 0x85, 0x01, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, - 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x29, 0x0a, - 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, - 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x22, 0x36, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, - 0x48, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, - 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x22, 0x29, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x7a, 0x0a, 0x15, 0x47, - 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x61, - 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, - 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x46, 0x0a, 0x0e, 0x46, 0x6c, 0x6f, 0x77, 0x41, - 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x22, - 0xf0, 0x01, 0x0a, 0x0c, 0x48, 0x75, 0x6e, 0x74, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x2e, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x0a, 0x61, - 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x6c, 0x6f, 0x77, 0x41, 0x73, 0x73, 0x69, - 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0a, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, - 0x6e, 0x74, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, - 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, - 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, + 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x12, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x55, 0x70, 0x6c, 0x6f, 0x61, + 0x64, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x77, 0x73, + 0x18, 0x13, 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x5f, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x04, 0x52, 0x14, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, + 0x12, 0x79, 0x0a, 0x07, 0x73, 0x74, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x42, 0x5f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x59, 0x12, 0x57, 0x49, 0x66, 0x20, 0x74, 0x68, + 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x73, 0x65, 0x74, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x69, 0x73, 0x20, 0x73, 0x74, 0x6f, 0x70, 0x70, + 0x65, 0x64, 0x2e, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x69, + 0x73, 0x20, 0x6d, 0x61, 0x6e, 0x69, 0x70, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x52, 0x07, 0x73, 0x74, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x12, 0x4a, 0x0a, 0x13, 0x61, + 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, + 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, + 0x61, 0x64, 0x73, 0x52, 0x12, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, + 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x12, 0x33, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x5f, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x63, 0x61, + 0x6e, 0x18, 0x11, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x6c, 0x61, 0x73, 0x74, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x63, 0x61, 0x6e, 0x22, 0xca, 0x0b, 0x0a, + 0x04, 0x48, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x09, 0x22, 0x07, + 0x48, 0x75, 0x6e, 0x74, 0x20, 0x49, 0x44, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x60, 0x0a, 0x0b, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x3f, + 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x39, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, + 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x57, 0x68, 0x65, 0x6e, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x68, + 0x75, 0x6e, 0x74, 0x20, 0x77, 0x61, 0x73, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x2e, + 0x22, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x54, 0x69, 0x6d, 0x65, 0x52, + 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1d, 0xe2, 0xfc, + 0xe3, 0xc4, 0x01, 0x17, 0x12, 0x15, 0x57, 0x68, 0x6f, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x3f, 0x52, 0x07, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x6f, 0x72, 0x12, 0x64, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x15, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x3f, + 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x57, + 0x68, 0x65, 0x6e, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x77, 0x61, + 0x73, 0x20, 0x61, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x6c, 0x79, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x65, 0x64, 0x2e, 0x22, 0x0a, 0x53, 0x74, 0x61, 0x72, 0x74, 0x20, 0x54, 0x69, 0x6d, 0x65, 0x52, + 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x57, 0x0a, 0x07, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x42, 0x3d, 0xe2, 0xfc, 0xe3, + 0xc4, 0x01, 0x37, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, + 0x12, 0x1b, 0x57, 0x68, 0x65, 0x6e, 0x20, 0x64, 0x6f, 0x65, 0x73, 0x20, 0x74, 0x68, 0x69, 0x73, + 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x3f, 0x22, 0x0b, 0x45, + 0x78, 0x70, 0x69, 0x72, 0x79, 0x20, 0x54, 0x69, 0x6d, 0x65, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x10, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1a, 0xe2, + 0xfc, 0xe3, 0xc4, 0x01, 0x14, 0x12, 0x12, 0x48, 0x75, 0x6e, 0x74, 0x27, 0x73, 0x20, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x68, 0x75, 0x6e, 0x74, 0x44, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, + 0x67, 0x73, 0x18, 0x17, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x88, + 0x01, 0x0a, 0x0d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, + 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x41, 0x72, 0x67, 0x73, 0x42, 0x45, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x3f, 0x12, 0x3d, 0x4c, 0x61, + 0x75, 0x6e, 0x63, 0x68, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x20, 0x69, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x73, 0x20, 0x74, 0x72, 0x75, 0x65, 0x52, 0x0c, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x8e, 0x01, 0x0a, 0x09, 0x63, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x42, 0x5a, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x54, 0x12, 0x42, 0x54, 0x68, 0x65, + 0x20, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, + 0x64, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x74, + 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x64, 0x2e, 0x22, + 0x0e, 0x48, 0x75, 0x6e, 0x74, 0x20, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x59, 0x0a, 0x0c, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, + 0x42, 0x36, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x30, 0x12, 0x2e, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x20, + 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x20, 0x6f, 0x66, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x73, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x77, 0x69, 0x6c, 0x6c, + 0x20, 0x72, 0x75, 0x6e, 0x20, 0x6f, 0x6e, 0x2e, 0x52, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x12, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x12, 0x4d, 0x0a, + 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, + 0x42, 0x2f, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x29, 0x12, 0x27, 0x41, 0x20, 0x6c, 0x69, 0x73, 0x74, + 0x20, 0x6f, 0x66, 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x20, 0x74, 0x68, + 0x69, 0x73, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x20, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x65, 0x73, + 0x2e, 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x12, 0x61, 0x0a, 0x10, + 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x18, 0x13, 0x20, 0x03, 0x28, 0x09, 0x42, 0x36, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x30, 0x12, 0x2e, + 0x41, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x20, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, + 0x68, 0x75, 0x6e, 0x74, 0x20, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x65, 0x73, 0x2e, 0x52, 0x0f, + 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, + 0x71, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x42, 0x48, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x42, 0x12, 0x40, 0x54, 0x68, 0x69, 0x73, 0x20, + 0x69, 0x73, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x68, 0x75, 0x6e, 0x74, 0x2e, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, + 0x20, 0x69, 0x73, 0x20, 0x6d, 0x61, 0x6e, 0x75, 0x70, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x20, + 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x47, 0x55, 0x49, 0x2e, 0x52, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x16, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x73, 0x22, 0xeb, 0x01, 0x0a, 0x05, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x55, 0x4e, 0x53, 0x45, 0x54, 0x10, 0x00, + 0x12, 0x48, 0x0a, 0x06, 0x50, 0x41, 0x55, 0x53, 0x45, 0x44, 0x10, 0x01, 0x1a, 0x3c, 0xea, 0xb9, + 0xcb, 0xb9, 0x01, 0x36, 0x48, 0x75, 0x6e, 0x74, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x6e, 0x6f, + 0x74, 0x20, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x62, + 0x65, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x2e, 0x12, 0x2d, 0x0a, 0x07, 0x52, 0x55, + 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x1a, 0x20, 0xea, 0xb9, 0xcb, 0xb9, 0x01, 0x1a, 0x48, + 0x75, 0x6e, 0x74, 0x20, 0x69, 0x73, 0x20, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x20, 0x61, + 0x6e, 0x64, 0x20, 0x72, 0x65, 0x61, 0x64, 0x79, 0x2e, 0x12, 0x24, 0x0a, 0x07, 0x53, 0x54, 0x4f, + 0x50, 0x50, 0x45, 0x44, 0x10, 0x03, 0x1a, 0x17, 0xea, 0xb9, 0xcb, 0xb9, 0x01, 0x11, 0x48, 0x75, + 0x6e, 0x74, 0x20, 0x68, 0x61, 0x73, 0x20, 0x73, 0x74, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x2e, 0x12, + 0x2b, 0x0a, 0x08, 0x41, 0x52, 0x43, 0x48, 0x49, 0x56, 0x45, 0x44, 0x10, 0x04, 0x1a, 0x1d, 0xea, + 0xb9, 0xcb, 0xb9, 0x01, 0x17, 0x48, 0x75, 0x6e, 0x74, 0x20, 0x68, 0x61, 0x73, 0x20, 0x62, 0x65, + 0x65, 0x6e, 0x20, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x2e, 0x12, 0x0b, 0x0a, 0x07, + 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x05, 0x22, 0x6a, 0x0a, 0x13, 0x48, 0x75, 0x6e, + 0x74, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, + 0x65, 0x12, 0x32, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, + 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xa6, 0x01, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, + 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, + 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, + 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, + 0x61, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, + 0x72, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x61, 0x72, + 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, + 0x63, 0x6c, 0x75, 0x64, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x12, 0x1f, 0x0a, + 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x4c, + 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x21, 0x0a, 0x05, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x52, 0x0a, 0x0e, + 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, + 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, + 0x64, 0x65, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x22, 0x7a, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, + 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, + 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x46, 0x0a, 0x0e, + 0x46, 0x6c, 0x6f, 0x77, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1b, + 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, + 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, + 0x6f, 0x77, 0x49, 0x64, 0x22, 0xb2, 0x02, 0x0a, 0x0c, 0x48, 0x75, 0x6e, 0x74, 0x4d, 0x75, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x26, + 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x48, 0x75, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0a, 0x61, 0x73, + 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x6c, 0x6f, 0x77, 0x41, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0a, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x1e, 0x0a, 0x08, 0x48, 0x75, 0x6e, + 0x74, 0x54, 0x61, 0x67, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, + 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, + 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, + 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1167,7 +1410,7 @@ func file_hunts_proto_rawDescGZIP() []byte { } var file_hunts_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_hunts_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_hunts_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_hunts_proto_goTypes = []interface{}{ (HuntOsCondition_OS)(0), // 0: proto.HuntOsCondition.OS (Hunt_State)(0), // 1: proto.Hunt.State @@ -1176,34 +1419,37 @@ var file_hunts_proto_goTypes = []interface{}{ (*HuntCondition)(nil), // 4: proto.HuntCondition (*HuntStats)(nil), // 5: proto.HuntStats (*Hunt)(nil), // 6: proto.Hunt - (*ListHuntsRequest)(nil), // 7: proto.ListHuntsRequest - (*ListHuntsResponse)(nil), // 8: proto.ListHuntsResponse - (*GetHuntRequest)(nil), // 9: proto.GetHuntRequest - (*GetHuntResultsRequest)(nil), // 10: proto.GetHuntResultsRequest - (*FlowAssignment)(nil), // 11: proto.FlowAssignment - (*HuntMutation)(nil), // 12: proto.HuntMutation - (*AvailableDownloads)(nil), // 13: proto.AvailableDownloads - (*proto.ArtifactCollectorArgs)(nil), // 14: proto.ArtifactCollectorArgs + (*HuntEstimateRequest)(nil), // 7: proto.HuntEstimateRequest + (*ListHuntsRequest)(nil), // 8: proto.ListHuntsRequest + (*ListHuntsResponse)(nil), // 9: proto.ListHuntsResponse + (*GetHuntRequest)(nil), // 10: proto.GetHuntRequest + (*GetHuntResultsRequest)(nil), // 11: proto.GetHuntResultsRequest + (*FlowAssignment)(nil), // 12: proto.FlowAssignment + (*HuntMutation)(nil), // 13: proto.HuntMutation + (*HuntTags)(nil), // 14: proto.HuntTags + (*AvailableDownloads)(nil), // 15: proto.AvailableDownloads + (*proto.ArtifactCollectorArgs)(nil), // 16: proto.ArtifactCollectorArgs } var file_hunts_proto_depIdxs = []int32{ 0, // 0: proto.HuntOsCondition.os:type_name -> proto.HuntOsCondition.OS 2, // 1: proto.HuntCondition.excluded_labels:type_name -> proto.HuntLabelCondition 2, // 2: proto.HuntCondition.labels:type_name -> proto.HuntLabelCondition 3, // 3: proto.HuntCondition.os:type_name -> proto.HuntOsCondition - 13, // 4: proto.HuntStats.available_downloads:type_name -> proto.AvailableDownloads - 14, // 5: proto.Hunt.start_request:type_name -> proto.ArtifactCollectorArgs + 15, // 4: proto.HuntStats.available_downloads:type_name -> proto.AvailableDownloads + 16, // 5: proto.Hunt.start_request:type_name -> proto.ArtifactCollectorArgs 4, // 6: proto.Hunt.condition:type_name -> proto.HuntCondition 5, // 7: proto.Hunt.stats:type_name -> proto.HuntStats 1, // 8: proto.Hunt.state:type_name -> proto.Hunt.State - 6, // 9: proto.ListHuntsResponse.items:type_name -> proto.Hunt - 5, // 10: proto.HuntMutation.stats:type_name -> proto.HuntStats - 1, // 11: proto.HuntMutation.state:type_name -> proto.Hunt.State - 11, // 12: proto.HuntMutation.assignment:type_name -> proto.FlowAssignment - 13, // [13:13] is the sub-list for method output_type - 13, // [13:13] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 4, // 9: proto.HuntEstimateRequest.condition:type_name -> proto.HuntCondition + 6, // 10: proto.ListHuntsResponse.items:type_name -> proto.Hunt + 5, // 11: proto.HuntMutation.stats:type_name -> proto.HuntStats + 1, // 12: proto.HuntMutation.state:type_name -> proto.Hunt.State + 12, // 13: proto.HuntMutation.assignment:type_name -> proto.FlowAssignment + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_hunts_proto_init() } @@ -1274,7 +1520,7 @@ func file_hunts_proto_init() { } } file_hunts_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListHuntsRequest); i { + switch v := v.(*HuntEstimateRequest); i { case 0: return &v.state case 1: @@ -1286,7 +1532,7 @@ func file_hunts_proto_init() { } } file_hunts_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListHuntsResponse); i { + switch v := v.(*ListHuntsRequest); i { case 0: return &v.state case 1: @@ -1298,7 +1544,7 @@ func file_hunts_proto_init() { } } file_hunts_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetHuntRequest); i { + switch v := v.(*ListHuntsResponse); i { case 0: return &v.state case 1: @@ -1310,7 +1556,7 @@ func file_hunts_proto_init() { } } file_hunts_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetHuntResultsRequest); i { + switch v := v.(*GetHuntRequest); i { case 0: return &v.state case 1: @@ -1322,7 +1568,7 @@ func file_hunts_proto_init() { } } file_hunts_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FlowAssignment); i { + switch v := v.(*GetHuntResultsRequest); i { case 0: return &v.state case 1: @@ -1334,6 +1580,18 @@ func file_hunts_proto_init() { } } file_hunts_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FlowAssignment); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_hunts_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*HuntMutation); i { case 0: return &v.state @@ -1345,6 +1603,18 @@ func file_hunts_proto_init() { return nil } } + file_hunts_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HuntTags); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_hunts_proto_msgTypes[2].OneofWrappers = []interface{}{ (*HuntCondition_Labels)(nil), @@ -1356,7 +1626,7 @@ func file_hunts_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_hunts_proto_rawDesc, NumEnums: 2, - NumMessages: 11, + NumMessages: 13, NumExtensions: 0, NumServices: 0, }, diff --git a/api/proto/hunts.proto b/api/proto/hunts.proto index d4dade9fe..dff465033 100644 --- a/api/proto/hunts.proto +++ b/api/proto/hunts.proto @@ -65,12 +65,21 @@ message HuntStats { friendly_name: "Total Clients with Errors", }]; + uint64 total_uploaded_bytes = 18; + uint64 total_collected_rows = 19; + uint64 total_finished_clients = 20; + bool stopped = 1 [(sem_type) = { description: "If this is set then the hunt is stopped. This field " "is manipulated by the hunt manager." }]; AvailableDownloads available_downloads = 2; + + // The last time the client table was scanned. This is used + // internally for a quick comparison when refreshing the hunt + // stats. + int64 last_client_table_scan = 17; } @@ -107,6 +116,9 @@ message Hunt { description: "Hunt's description", }]; + // Hunts can be tagged. + repeated string tags = 23; + ArtifactCollectorArgs start_request = 16 [(sem_type) = { description: "Launch this collection on the client if the condition is true", }]; @@ -136,11 +148,23 @@ message Hunt { RUNNING = 2 [(description) = "Hunt is running and ready."]; STOPPED = 3 [(description) = "Hunt has stopped."]; ARCHIVED = 4 [(description) = "Hunt has been archived."]; + + // Set internally when the hunt is being deleted. + DELETED = 5; }; State state = 8 [(sem_type) = { description: "This is state of the hunt. This field is manupulated by the GUI." }]; + + // A list of the org IDs that the hunt will be launched on + repeated string org_ids = 22; +} + +message HuntEstimateRequest { + // Only show clients that were active this many seconds ago. + uint64 last_active = 1; + HuntCondition condition = 4; } message ListHuntsRequest { @@ -150,14 +174,18 @@ message ListHuntsRequest { // If specified we return a partial structure. bool summary = 4; bool include_archived = 3; + + string user_filter = 5; } message ListHuntsResponse { + int64 total = 2; repeated Hunt items = 1; } message GetHuntRequest { string hunt_id = 1; + bool include_request = 2; } message GetHuntResultsRequest { @@ -178,9 +206,19 @@ message HuntMutation { string description = 3; Hunt.State state = 4; uint64 start_time = 5; + uint64 expires = 7; // A mutation can directly assign an existing flow to the // hunt. This allows a flow to be rerun and added to the hunt // later. FlowAssignment assignment = 6; + + repeated string tags = 8; + + // The user who is initiating the mutation. + string user = 9; +} + +message HuntTags { + repeated string tags = 1; } \ No newline at end of file diff --git a/api/proto/notebooks.pb.go b/api/proto/notebooks.pb.go index 20b17b547..48c391c24 100644 --- a/api/proto/notebooks.pb.go +++ b/api/proto/notebooks.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: notebooks.proto package proto @@ -11,7 +8,9 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" - proto "www.velocidex.com/golang/velociraptor/artifacts/proto" + proto2 "www.velocidex.com/golang/velociraptor/actions/proto" + proto1 "www.velocidex.com/golang/velociraptor/artifacts/proto" + proto "www.velocidex.com/golang/velociraptor/flows/proto" ) const ( @@ -21,6 +20,61 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type ReformatVQLMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Vql string `protobuf:"bytes,1,opt,name=vql,proto3" json:"vql,omitempty"` + Artifact string `protobuf:"bytes,2,opt,name=artifact,proto3" json:"artifact,omitempty"` +} + +func (x *ReformatVQLMessage) Reset() { + *x = ReformatVQLMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_notebooks_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReformatVQLMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReformatVQLMessage) ProtoMessage() {} + +func (x *ReformatVQLMessage) ProtoReflect() protoreflect.Message { + mi := &file_notebooks_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReformatVQLMessage.ProtoReflect.Descriptor instead. +func (*ReformatVQLMessage) Descriptor() ([]byte, []int) { + return file_notebooks_proto_rawDescGZIP(), []int{0} +} + +func (x *ReformatVQLMessage) GetVql() string { + if x != nil { + return x.Vql + } + return "" +} + +func (x *ReformatVQLMessage) GetArtifact() string { + if x != nil { + return x.Artifact + } + return "" +} + type Env struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -33,7 +87,7 @@ type Env struct { func (x *Env) Reset() { *x = Env{} if protoimpl.UnsafeEnabled { - mi := &file_notebooks_proto_msgTypes[0] + mi := &file_notebooks_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -46,7 +100,7 @@ func (x *Env) String() string { func (*Env) ProtoMessage() {} func (x *Env) ProtoReflect() protoreflect.Message { - mi := &file_notebooks_proto_msgTypes[0] + mi := &file_notebooks_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -59,7 +113,7 @@ func (x *Env) ProtoReflect() protoreflect.Message { // Deprecated: Use Env.ProtoReflect.Descriptor instead. func (*Env) Descriptor() ([]byte, []int) { - return file_notebooks_proto_rawDescGZIP(), []int{0} + return file_notebooks_proto_rawDescGZIP(), []int{1} } func (x *Env) GetKey() string { @@ -81,14 +135,15 @@ type NotebookExportRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - NotebookId string `protobuf:"bytes,1,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` - Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + NotebookId string `protobuf:"bytes,1,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + PreferredName string `protobuf:"bytes,3,opt,name=preferred_name,json=preferredName,proto3" json:"preferred_name,omitempty"` } func (x *NotebookExportRequest) Reset() { *x = NotebookExportRequest{} if protoimpl.UnsafeEnabled { - mi := &file_notebooks_proto_msgTypes[1] + mi := &file_notebooks_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -101,7 +156,7 @@ func (x *NotebookExportRequest) String() string { func (*NotebookExportRequest) ProtoMessage() {} func (x *NotebookExportRequest) ProtoReflect() protoreflect.Message { - mi := &file_notebooks_proto_msgTypes[1] + mi := &file_notebooks_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -114,7 +169,7 @@ func (x *NotebookExportRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NotebookExportRequest.ProtoReflect.Descriptor instead. func (*NotebookExportRequest) Descriptor() ([]byte, []int) { - return file_notebooks_proto_rawDescGZIP(), []int{1} + return file_notebooks_proto_rawDescGZIP(), []int{2} } func (x *NotebookExportRequest) GetNotebookId() string { @@ -131,25 +186,41 @@ func (x *NotebookExportRequest) GetType() string { return "" } +func (x *NotebookExportRequest) GetPreferredName() string { + if x != nil { + return x.PreferredName + } + return "" +} + +// Message sent to the notebook processor ro request a cell recalc. type NotebookCellRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - NotebookId string `protobuf:"bytes,1,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` - CellId string `protobuf:"bytes,2,opt,name=cell_id,json=cellId,proto3" json:"cell_id,omitempty"` - Input string `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` - Offset uint64 `protobuf:"varint,4,opt,name=offset,proto3" json:"offset,omitempty"` - Count uint64 `protobuf:"varint,5,opt,name=count,proto3" json:"count,omitempty"` - Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` - CurrentlyEditing bool `protobuf:"varint,8,opt,name=currently_editing,json=currentlyEditing,proto3" json:"currently_editing,omitempty"` - Env []*Env `protobuf:"bytes,9,rep,name=env,proto3" json:"env,omitempty"` + NotebookId string `protobuf:"bytes,1,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` + CellId string `protobuf:"bytes,2,opt,name=cell_id,json=cellId,proto3" json:"cell_id,omitempty"` + Version string `protobuf:"bytes,12,opt,name=version,proto3" json:"version,omitempty"` + AvailableVersions []string `protobuf:"bytes,13,rep,name=available_versions,json=availableVersions,proto3" json:"available_versions,omitempty"` + Input string `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + Output string `protobuf:"bytes,14,opt,name=output,proto3" json:"output,omitempty"` + Name string `protobuf:"bytes,11,opt,name=name,proto3" json:"name,omitempty"` + Offset uint64 `protobuf:"varint,4,opt,name=offset,proto3" json:"offset,omitempty"` + Count uint64 `protobuf:"varint,5,opt,name=count,proto3" json:"count,omitempty"` + Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` + CurrentlyEditing bool `protobuf:"varint,8,opt,name=currently_editing,json=currentlyEditing,proto3" json:"currently_editing,omitempty"` + Env []*Env `protobuf:"bytes,9,rep,name=env,proto3" json:"env,omitempty"` + IncludeUploads bool `protobuf:"varint,10,opt,name=include_uploads,json=includeUploads,proto3" json:"include_uploads,omitempty"` + IncludeTimelines bool `protobuf:"varint,16,opt,name=include_timelines,json=includeTimelines,proto3" json:"include_timelines,omitempty"` + // If this is set schedule the calculation syncronously. + Sync bool `protobuf:"varint,15,opt,name=sync,proto3" json:"sync,omitempty"` } func (x *NotebookCellRequest) Reset() { *x = NotebookCellRequest{} if protoimpl.UnsafeEnabled { - mi := &file_notebooks_proto_msgTypes[2] + mi := &file_notebooks_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -162,7 +233,7 @@ func (x *NotebookCellRequest) String() string { func (*NotebookCellRequest) ProtoMessage() {} func (x *NotebookCellRequest) ProtoReflect() protoreflect.Message { - mi := &file_notebooks_proto_msgTypes[2] + mi := &file_notebooks_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -175,7 +246,7 @@ func (x *NotebookCellRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NotebookCellRequest.ProtoReflect.Descriptor instead. func (*NotebookCellRequest) Descriptor() ([]byte, []int) { - return file_notebooks_proto_rawDescGZIP(), []int{2} + return file_notebooks_proto_rawDescGZIP(), []int{3} } func (x *NotebookCellRequest) GetNotebookId() string { @@ -192,6 +263,20 @@ func (x *NotebookCellRequest) GetCellId() string { return "" } +func (x *NotebookCellRequest) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *NotebookCellRequest) GetAvailableVersions() []string { + if x != nil { + return x.AvailableVersions + } + return nil +} + func (x *NotebookCellRequest) GetInput() string { if x != nil { return x.Input @@ -199,6 +284,20 @@ func (x *NotebookCellRequest) GetInput() string { return "" } +func (x *NotebookCellRequest) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +func (x *NotebookCellRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + func (x *NotebookCellRequest) GetOffset() uint64 { if x != nil { return x.Offset @@ -234,21 +333,45 @@ func (x *NotebookCellRequest) GetEnv() []*Env { return nil } +func (x *NotebookCellRequest) GetIncludeUploads() bool { + if x != nil { + return x.IncludeUploads + } + return false +} + +func (x *NotebookCellRequest) GetIncludeTimelines() bool { + if x != nil { + return x.IncludeTimelines + } + return false +} + +func (x *NotebookCellRequest) GetSync() bool { + if x != nil { + return x.Sync + } + return false +} + type NotebookContext struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - HuntId string `protobuf:"bytes,2,opt,name=hunt_id,json=huntId,proto3" json:"hunt_id,omitempty"` - FlowId string `protobuf:"bytes,3,opt,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` - ClientId string `protobuf:"bytes,4,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + HuntId string `protobuf:"bytes,2,opt,name=hunt_id,json=huntId,proto3" json:"hunt_id,omitempty"` + FlowId string `protobuf:"bytes,3,opt,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` + ClientId string `protobuf:"bytes,4,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + EventArtifact string `protobuf:"bytes,5,opt,name=event_artifact,json=eventArtifact,proto3" json:"event_artifact,omitempty"` + StartTime int64 `protobuf:"varint,6,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + EndTime int64 `protobuf:"varint,7,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` } func (x *NotebookContext) Reset() { *x = NotebookContext{} if protoimpl.UnsafeEnabled { - mi := &file_notebooks_proto_msgTypes[3] + mi := &file_notebooks_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -261,7 +384,7 @@ func (x *NotebookContext) String() string { func (*NotebookContext) ProtoMessage() {} func (x *NotebookContext) ProtoReflect() protoreflect.Message { - mi := &file_notebooks_proto_msgTypes[3] + mi := &file_notebooks_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -274,7 +397,7 @@ func (x *NotebookContext) ProtoReflect() protoreflect.Message { // Deprecated: Use NotebookContext.ProtoReflect.Descriptor instead. func (*NotebookContext) Descriptor() ([]byte, []int) { - return file_notebooks_proto_rawDescGZIP(), []int{3} + return file_notebooks_proto_rawDescGZIP(), []int{4} } func (x *NotebookContext) GetType() string { @@ -305,6 +428,28 @@ func (x *NotebookContext) GetClientId() string { return "" } +func (x *NotebookContext) GetEventArtifact() string { + if x != nil { + return x.EventArtifact + } + return "" +} + +func (x *NotebookContext) GetStartTime() int64 { + if x != nil { + return x.StartTime + } + return 0 +} + +func (x *NotebookContext) GetEndTime() int64 { + if x != nil { + return x.EndTime + } + return 0 +} + +// Represents an entire notebook. type NotebookMetadata struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -317,6 +462,16 @@ type NotebookMetadata struct { Context *NotebookContext `protobuf:"bytes,16,opt,name=context,proto3" json:"context,omitempty"` // A list of usernames that have access to this notebook. Collaborators []string `protobuf:"bytes,12,rep,name=collaborators,proto3" json:"collaborators,omitempty"` + // A list of NOTEBOOK artifacts to create the notebook with. + Artifacts []string `protobuf:"bytes,20,rep,name=artifacts,proto3" json:"artifacts,omitempty"` + // Each notebook template can be passed parameters + Specs []*proto.ArtifactSpec `protobuf:"bytes,21,rep,name=specs,proto3" json:"specs,omitempty"` + // Notebooks can have typed parameters which are injected into + // every cell. + Parameters []*proto1.ArtifactParameter `protobuf:"bytes,22,rep,name=parameters,proto3" json:"parameters,omitempty"` + // These queries will be run before each cell is evaluated in + // order to set up the parameters. + Requests []*proto2.VQLCollectorArgs `protobuf:"bytes,23,rep,name=requests,proto3" json:"requests,omitempty"` // If this is set, the notebook is public. Public bool `protobuf:"varint,13,opt,name=public,proto3" json:"public,omitempty"` CreatedTime int64 `protobuf:"varint,4,opt,name=created_time,json=createdTime,proto3" json:"created_time,omitempty"` @@ -328,17 +483,21 @@ type NotebookMetadata struct { LatestCellId string `protobuf:"bytes,8,opt,name=latest_cell_id,json=latestCellId,proto3" json:"latest_cell_id,omitempty"` Hidden bool `protobuf:"varint,9,opt,name=hidden,proto3" json:"hidden,omitempty"` AvailableDownloads *AvailableDownloads `protobuf:"bytes,10,opt,name=available_downloads,json=availableDownloads,proto3" json:"available_downloads,omitempty"` + AvailableUploads *AvailableDownloads `protobuf:"bytes,18,opt,name=available_uploads,json=availableUploads,proto3" json:"available_uploads,omitempty"` // These environment variables will be populated into each // notebook cell in this notebook. - Env []*Env `protobuf:"bytes,14,rep,name=env,proto3" json:"env,omitempty"` - Timelines []string `protobuf:"bytes,15,rep,name=timelines,proto3" json:"timelines,omitempty"` - ColumnTypes []*proto.ColumnType `protobuf:"bytes,17,rep,name=column_types,json=columnTypes,proto3" json:"column_types,omitempty"` + Env []*Env `protobuf:"bytes,14,rep,name=env,proto3" json:"env,omitempty"` + Timelines []string `protobuf:"bytes,15,rep,name=timelines,proto3" json:"timelines,omitempty"` + ColumnTypes []*proto1.ColumnType `protobuf:"bytes,17,rep,name=column_types,json=columnTypes,proto3" json:"column_types,omitempty"` + // Cells that are not immediately included but may be included by + // the GUI as suggestions. + Suggestions []*NotebookCellRequest `protobuf:"bytes,19,rep,name=suggestions,proto3" json:"suggestions,omitempty"` } func (x *NotebookMetadata) Reset() { *x = NotebookMetadata{} if protoimpl.UnsafeEnabled { - mi := &file_notebooks_proto_msgTypes[4] + mi := &file_notebooks_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -351,7 +510,7 @@ func (x *NotebookMetadata) String() string { func (*NotebookMetadata) ProtoMessage() {} func (x *NotebookMetadata) ProtoReflect() protoreflect.Message { - mi := &file_notebooks_proto_msgTypes[4] + mi := &file_notebooks_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -364,7 +523,7 @@ func (x *NotebookMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use NotebookMetadata.ProtoReflect.Descriptor instead. func (*NotebookMetadata) Descriptor() ([]byte, []int) { - return file_notebooks_proto_rawDescGZIP(), []int{4} + return file_notebooks_proto_rawDescGZIP(), []int{5} } func (x *NotebookMetadata) GetName() string { @@ -402,6 +561,34 @@ func (x *NotebookMetadata) GetCollaborators() []string { return nil } +func (x *NotebookMetadata) GetArtifacts() []string { + if x != nil { + return x.Artifacts + } + return nil +} + +func (x *NotebookMetadata) GetSpecs() []*proto.ArtifactSpec { + if x != nil { + return x.Specs + } + return nil +} + +func (x *NotebookMetadata) GetParameters() []*proto1.ArtifactParameter { + if x != nil { + return x.Parameters + } + return nil +} + +func (x *NotebookMetadata) GetRequests() []*proto2.VQLCollectorArgs { + if x != nil { + return x.Requests + } + return nil +} + func (x *NotebookMetadata) GetPublic() bool { if x != nil { return x.Public @@ -465,6 +652,13 @@ func (x *NotebookMetadata) GetAvailableDownloads() *AvailableDownloads { return nil } +func (x *NotebookMetadata) GetAvailableUploads() *AvailableDownloads { + if x != nil { + return x.AvailableUploads + } + return nil +} + func (x *NotebookMetadata) GetEnv() []*Env { if x != nil { return x.Env @@ -479,13 +673,20 @@ func (x *NotebookMetadata) GetTimelines() []string { return nil } -func (x *NotebookMetadata) GetColumnTypes() []*proto.ColumnType { +func (x *NotebookMetadata) GetColumnTypes() []*proto1.ColumnType { if x != nil { return x.ColumnTypes } return nil } +func (x *NotebookMetadata) GetSuggestions() []*NotebookCellRequest { + if x != nil { + return x.Suggestions + } + return nil +} + type Notebooks struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -497,7 +698,7 @@ type Notebooks struct { func (x *Notebooks) Reset() { *x = Notebooks{} if protoimpl.UnsafeEnabled { - mi := &file_notebooks_proto_msgTypes[5] + mi := &file_notebooks_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -510,7 +711,7 @@ func (x *Notebooks) String() string { func (*Notebooks) ProtoMessage() {} func (x *Notebooks) ProtoReflect() protoreflect.Message { - mi := &file_notebooks_proto_msgTypes[5] + mi := &file_notebooks_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -523,7 +724,7 @@ func (x *Notebooks) ProtoReflect() protoreflect.Message { // Deprecated: Use Notebooks.ProtoReflect.Descriptor instead. func (*Notebooks) Descriptor() ([]byte, []int) { - return file_notebooks_proto_rawDescGZIP(), []int{5} + return file_notebooks_proto_rawDescGZIP(), []int{6} } func (x *Notebooks) GetItems() []*NotebookMetadata { @@ -538,24 +739,33 @@ type NotebookCell struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Input string `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` - Output string `protobuf:"bytes,2,opt,name=output,proto3" json:"output,omitempty"` - Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` - CellId string `protobuf:"bytes,4,opt,name=cell_id,json=cellId,proto3" json:"cell_id,omitempty"` - Messages []string `protobuf:"bytes,5,rep,name=messages,proto3" json:"messages,omitempty"` - Timestamp int64 `protobuf:"varint,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - Duration int64 `protobuf:"varint,10,opt,name=duration,proto3" json:"duration,omitempty"` + NotebookId string `protobuf:"bytes,16,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` + Input string `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` + Output string `protobuf:"bytes,2,opt,name=output,proto3" json:"output,omitempty"` + // A short summary of the notebook cell. + Summary string `protobuf:"bytes,17,opt,name=summary,proto3" json:"summary,omitempty"` + Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + CellId string `protobuf:"bytes,4,opt,name=cell_id,json=cellId,proto3" json:"cell_id,omitempty"` + Messages []string `protobuf:"bytes,5,rep,name=messages,proto3" json:"messages,omitempty"` + // True if there are more messages than are included in the + // messages field above. + MoreMessages bool `protobuf:"varint,12,opt,name=more_messages,json=moreMessages,proto3" json:"more_messages,omitempty"` + Timestamp int64 `protobuf:"varint,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Duration int64 `protobuf:"varint,10,opt,name=duration,proto3" json:"duration,omitempty"` // The type of this cell. - Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"` - CurrentlyEditing bool `protobuf:"varint,8,opt,name=currently_editing,json=currentlyEditing,proto3" json:"currently_editing,omitempty"` - Calculating bool `protobuf:"varint,9,opt,name=calculating,proto3" json:"calculating,omitempty"` - Env []*Env `protobuf:"bytes,11,rep,name=env,proto3" json:"env,omitempty"` + Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"` + CurrentlyEditing bool `protobuf:"varint,8,opt,name=currently_editing,json=currentlyEditing,proto3" json:"currently_editing,omitempty"` + Calculating bool `protobuf:"varint,9,opt,name=calculating,proto3" json:"calculating,omitempty"` + Env []*Env `protobuf:"bytes,11,rep,name=env,proto3" json:"env,omitempty"` + Error string `protobuf:"bytes,13,opt,name=error,proto3" json:"error,omitempty"` + CurrentVersion string `protobuf:"bytes,14,opt,name=current_version,json=currentVersion,proto3" json:"current_version,omitempty"` + AvailableVersions []string `protobuf:"bytes,15,rep,name=available_versions,json=availableVersions,proto3" json:"available_versions,omitempty"` } func (x *NotebookCell) Reset() { *x = NotebookCell{} if protoimpl.UnsafeEnabled { - mi := &file_notebooks_proto_msgTypes[6] + mi := &file_notebooks_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -568,7 +778,7 @@ func (x *NotebookCell) String() string { func (*NotebookCell) ProtoMessage() {} func (x *NotebookCell) ProtoReflect() protoreflect.Message { - mi := &file_notebooks_proto_msgTypes[6] + mi := &file_notebooks_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -581,7 +791,14 @@ func (x *NotebookCell) ProtoReflect() protoreflect.Message { // Deprecated: Use NotebookCell.ProtoReflect.Descriptor instead. func (*NotebookCell) Descriptor() ([]byte, []int) { - return file_notebooks_proto_rawDescGZIP(), []int{6} + return file_notebooks_proto_rawDescGZIP(), []int{7} +} + +func (x *NotebookCell) GetNotebookId() string { + if x != nil { + return x.NotebookId + } + return "" } func (x *NotebookCell) GetInput() string { @@ -598,6 +815,13 @@ func (x *NotebookCell) GetOutput() string { return "" } +func (x *NotebookCell) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + func (x *NotebookCell) GetData() string { if x != nil { return x.Data @@ -619,6 +843,13 @@ func (x *NotebookCell) GetMessages() []string { return nil } +func (x *NotebookCell) GetMoreMessages() bool { + if x != nil { + return x.MoreMessages + } + return false +} + func (x *NotebookCell) GetTimestamp() int64 { if x != nil { return x.Timestamp @@ -661,20 +892,44 @@ func (x *NotebookCell) GetEnv() []*Env { return nil } +func (x *NotebookCell) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *NotebookCell) GetCurrentVersion() string { + if x != nil { + return x.CurrentVersion + } + return "" +} + +func (x *NotebookCell) GetAvailableVersions() []string { + if x != nil { + return x.AvailableVersions + } + return nil +} + type NotebookFileUploadRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` - Filename string `protobuf:"bytes,2,opt,name=filename,proto3" json:"filename,omitempty"` - NotebookId string `protobuf:"bytes,3,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` + Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + Filename string `protobuf:"bytes,2,opt,name=filename,proto3" json:"filename,omitempty"` + DisableAttachmentId bool `protobuf:"varint,6,opt,name=disable_attachment_id,json=disableAttachmentId,proto3" json:"disable_attachment_id,omitempty"` + Components []string `protobuf:"bytes,4,rep,name=components,proto3" json:"components,omitempty"` + NotebookId string `protobuf:"bytes,3,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` + CellId string `protobuf:"bytes,5,opt,name=cell_id,json=cellId,proto3" json:"cell_id,omitempty"` } func (x *NotebookFileUploadRequest) Reset() { *x = NotebookFileUploadRequest{} if protoimpl.UnsafeEnabled { - mi := &file_notebooks_proto_msgTypes[7] + mi := &file_notebooks_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -687,7 +942,7 @@ func (x *NotebookFileUploadRequest) String() string { func (*NotebookFileUploadRequest) ProtoMessage() {} func (x *NotebookFileUploadRequest) ProtoReflect() protoreflect.Message { - mi := &file_notebooks_proto_msgTypes[7] + mi := &file_notebooks_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -700,7 +955,7 @@ func (x *NotebookFileUploadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NotebookFileUploadRequest.ProtoReflect.Descriptor instead. func (*NotebookFileUploadRequest) Descriptor() ([]byte, []int) { - return file_notebooks_proto_rawDescGZIP(), []int{7} + return file_notebooks_proto_rawDescGZIP(), []int{8} } func (x *NotebookFileUploadRequest) GetData() string { @@ -717,6 +972,20 @@ func (x *NotebookFileUploadRequest) GetFilename() string { return "" } +func (x *NotebookFileUploadRequest) GetDisableAttachmentId() bool { + if x != nil { + return x.DisableAttachmentId + } + return false +} + +func (x *NotebookFileUploadRequest) GetComponents() []string { + if x != nil { + return x.Components + } + return nil +} + func (x *NotebookFileUploadRequest) GetNotebookId() string { if x != nil { return x.NotebookId @@ -724,18 +993,27 @@ func (x *NotebookFileUploadRequest) GetNotebookId() string { return "" } +func (x *NotebookFileUploadRequest) GetCellId() string { + if x != nil { + return x.CellId + } + return "" +} + type NotebookFileUploadResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Filename string `protobuf:"bytes,2,opt,name=filename,proto3" json:"filename,omitempty"` + MimeType string `protobuf:"bytes,3,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"` } func (x *NotebookFileUploadResponse) Reset() { *x = NotebookFileUploadResponse{} if protoimpl.UnsafeEnabled { - mi := &file_notebooks_proto_msgTypes[8] + mi := &file_notebooks_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -748,7 +1026,7 @@ func (x *NotebookFileUploadResponse) String() string { func (*NotebookFileUploadResponse) ProtoMessage() {} func (x *NotebookFileUploadResponse) ProtoReflect() protoreflect.Message { - mi := &file_notebooks_proto_msgTypes[8] + mi := &file_notebooks_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -761,7 +1039,7 @@ func (x *NotebookFileUploadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use NotebookFileUploadResponse.ProtoReflect.Descriptor instead. func (*NotebookFileUploadResponse) Descriptor() ([]byte, []int) { - return file_notebooks_proto_rawDescGZIP(), []int{8} + return file_notebooks_proto_rawDescGZIP(), []int{9} } func (x *NotebookFileUploadResponse) GetUrl() string { @@ -771,122 +1049,210 @@ func (x *NotebookFileUploadResponse) GetUrl() string { return "" } +func (x *NotebookFileUploadResponse) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *NotebookFileUploadResponse) GetMimeType() string { + if x != nil { + return x.MimeType + } + return "" +} + var File_notebooks_proto protoreflect.FileDescriptor var file_notebooks_proto_rawDesc = []byte{ 0x0a, 0x0f, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0b, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2d, 0x0a, 0x03, 0x45, 0x6e, 0x76, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4c, 0x0a, 0x15, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, - 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, - 0x0b, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x49, 0x64, 0x12, 0x12, - 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, - 0x70, 0x65, 0x22, 0xf2, 0x01, 0x0a, 0x13, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, - 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, - 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x63, - 0x65, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x65, - 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, - 0x66, 0x73, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, - 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2b, 0x0a, 0x11, - 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x5f, 0x65, 0x64, 0x69, 0x74, 0x69, 0x6e, - 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, - 0x6c, 0x79, 0x45, 0x64, 0x69, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x0a, 0x03, 0x65, 0x6e, 0x76, - 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, - 0x6e, 0x76, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x22, 0x74, 0x0a, 0x0f, 0x4e, 0x6f, 0x74, 0x65, 0x62, - 0x6f, 0x6f, 0x6b, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, - 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x17, - 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, - 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, - 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x87, 0x05, - 0x0a, 0x10, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x6f, 0x72, 0x12, 0x30, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x10, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, - 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, - 0x74, 0x65, 0x78, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x61, 0x62, 0x6f, 0x72, - 0x61, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6c, - 0x6c, 0x61, 0x62, 0x6f, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, - 0x62, 0x6c, 0x69, 0x63, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, - 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6d, 0x6f, - 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, - 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x63, - 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, - 0x73, 0x12, 0x38, 0x0a, 0x0d, 0x63, 0x65, 0x6c, 0x6c, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x0c, 0x63, - 0x65, 0x6c, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x24, 0x0a, 0x0e, 0x6c, - 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x65, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x43, 0x65, 0x6c, 0x6c, 0x49, - 0x64, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x12, 0x4a, 0x0a, 0x13, 0x61, 0x76, 0x61, - 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, - 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, - 0x73, 0x52, 0x12, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, - 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x12, 0x1c, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x0e, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6e, 0x76, 0x52, 0x03, - 0x65, 0x6e, 0x76, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, - 0x18, 0x0f, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, - 0x73, 0x12, 0x34, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x75, - 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0x3a, 0x0a, 0x09, 0x4e, 0x6f, 0x74, 0x65, 0x62, - 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x2d, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, - 0x62, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x05, 0x69, 0x74, - 0x65, 0x6d, 0x73, 0x22, 0xc0, 0x02, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, - 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, - 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, - 0x75, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x65, 0x6c, 0x6c, 0x5f, 0x69, - 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x65, 0x6c, 0x6c, 0x49, 0x64, 0x12, - 0x1a, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, - 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x64, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x75, 0x72, - 0x72, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x5f, 0x65, 0x64, 0x69, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x45, - 0x64, 0x69, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, - 0x61, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x63, 0x61, 0x6c, - 0x63, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, - 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6e, - 0x76, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x22, 0x6c, 0x0a, 0x19, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, - 0x6f, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x5f, - 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, - 0x6f, 0x6b, 0x49, 0x64, 0x22, 0x2e, 0x0a, 0x1a, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, - 0x46, 0x69, 0x6c, 0x65, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x75, 0x72, 0x6c, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, - 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, - 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x71, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x1e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x24, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, + 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0b, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x42, 0x0a, 0x12, 0x52, 0x65, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x56, 0x51, 0x4c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x71, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x76, 0x71, 0x6c, 0x12, 0x1a, 0x0a, 0x08, + 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x2d, 0x0a, 0x03, 0x45, 0x6e, 0x76, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x73, 0x0a, 0x15, 0x4e, 0x6f, 0x74, 0x65, 0x62, + 0x6f, 0x6f, 0x6b, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x49, + 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, + 0x65, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, + 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xd1, 0x03, 0x0a, + 0x13, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x6f, 0x74, 0x65, 0x62, + 0x6f, 0x6f, 0x6b, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x65, 0x6c, 0x6c, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x65, 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x18, + 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x12, 0x61, 0x76, 0x61, 0x69, + 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x16, 0x0a, + 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, + 0x73, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x63, + 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x5f, 0x65, 0x64, 0x69, 0x74, 0x69, 0x6e, 0x67, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x6c, + 0x79, 0x45, 0x64, 0x69, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, + 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6e, + 0x76, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, + 0x65, 0x5f, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x12, + 0x2b, 0x0a, 0x11, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6c, + 0x69, 0x6e, 0x65, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x69, 0x6e, 0x63, 0x6c, + 0x75, 0x64, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, + 0x73, 0x79, 0x6e, 0x63, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x73, 0x79, 0x6e, 0x63, + 0x22, 0xd5, 0x01, 0x0a, 0x0f, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x6f, 0x6e, + 0x74, 0x65, 0x78, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, + 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x76, 0x65, 0x6e, 0x74, + 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x1d, + 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, + 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xc5, 0x07, 0x0a, 0x10, 0x4e, 0x6f, 0x74, + 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x30, 0x0a, + 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, + 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, + 0x24, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x61, 0x62, 0x6f, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x73, + 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x61, 0x62, 0x6f, 0x72, + 0x61, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x73, 0x12, 0x29, 0x0a, 0x05, 0x73, 0x70, 0x65, 0x63, 0x73, 0x18, 0x15, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x53, 0x70, 0x65, 0x63, 0x52, 0x05, 0x73, 0x70, 0x65, 0x63, 0x73, 0x12, 0x38, + 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x16, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x33, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x73, 0x18, 0x17, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, + 0x72, 0x67, 0x73, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x16, 0x0a, + 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x6f, 0x64, 0x69, + 0x66, 0x69, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0c, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1f, 0x0a, + 0x0b, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x49, 0x64, 0x12, 0x14, + 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x63, + 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x38, 0x0a, 0x0d, 0x63, 0x65, 0x6c, 0x6c, 0x5f, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, + 0x52, 0x0c, 0x63, 0x65, 0x6c, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x24, + 0x0a, 0x0e, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x65, 0x6c, 0x6c, 0x5f, 0x69, 0x64, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x43, 0x65, + 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x12, 0x4a, 0x0a, 0x13, + 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, + 0x61, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, + 0x6f, 0x61, 0x64, 0x73, 0x52, 0x12, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, + 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x12, 0x46, 0x0a, 0x11, 0x61, 0x76, 0x61, 0x69, + 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x12, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x76, 0x61, 0x69, + 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x52, 0x10, + 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x73, + 0x12, 0x1c, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6e, 0x76, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x1c, + 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x0c, + 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x11, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, + 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, + 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x0b, 0x73, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x13, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x52, 0x0b, 0x73, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x22, 0x3a, 0x0a, 0x09, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x2d, 0x0a, + 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x8e, 0x04, 0x0a, + 0x0c, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x1f, 0x0a, + 0x0b, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x10, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x49, 0x64, 0x12, 0x14, + 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x18, 0x0a, 0x07, + 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, + 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x65, + 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x65, 0x6c, + 0x6c, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, + 0x23, 0x0a, 0x0d, 0x6d, 0x6f, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6d, 0x6f, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x5f, + 0x65, 0x64, 0x69, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x63, + 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x45, 0x64, 0x69, 0x74, 0x69, 0x6e, 0x67, 0x12, + 0x20, 0x0a, 0x0b, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x6e, + 0x67, 0x12, 0x1c, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6e, 0x76, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, + 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, + 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, + 0x0a, 0x12, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x61, 0x76, 0x61, 0x69, + 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd9, 0x01, + 0x0a, 0x19, 0x4e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x55, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, + 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, + 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, + 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x49, 0x64, + 0x12, 0x17, 0x0a, 0x07, 0x63, 0x65, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x63, 0x65, 0x6c, 0x6c, 0x49, 0x64, 0x22, 0x67, 0x0a, 0x1a, 0x4e, 0x6f, 0x74, + 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, + 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, + 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, + 0x70, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, + 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, + 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -901,34 +1267,43 @@ func file_notebooks_proto_rawDescGZIP() []byte { return file_notebooks_proto_rawDescData } -var file_notebooks_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_notebooks_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_notebooks_proto_goTypes = []interface{}{ - (*Env)(nil), // 0: proto.Env - (*NotebookExportRequest)(nil), // 1: proto.NotebookExportRequest - (*NotebookCellRequest)(nil), // 2: proto.NotebookCellRequest - (*NotebookContext)(nil), // 3: proto.NotebookContext - (*NotebookMetadata)(nil), // 4: proto.NotebookMetadata - (*Notebooks)(nil), // 5: proto.Notebooks - (*NotebookCell)(nil), // 6: proto.NotebookCell - (*NotebookFileUploadRequest)(nil), // 7: proto.NotebookFileUploadRequest - (*NotebookFileUploadResponse)(nil), // 8: proto.NotebookFileUploadResponse - (*AvailableDownloads)(nil), // 9: proto.AvailableDownloads - (*proto.ColumnType)(nil), // 10: proto.ColumnType + (*ReformatVQLMessage)(nil), // 0: proto.ReformatVQLMessage + (*Env)(nil), // 1: proto.Env + (*NotebookExportRequest)(nil), // 2: proto.NotebookExportRequest + (*NotebookCellRequest)(nil), // 3: proto.NotebookCellRequest + (*NotebookContext)(nil), // 4: proto.NotebookContext + (*NotebookMetadata)(nil), // 5: proto.NotebookMetadata + (*Notebooks)(nil), // 6: proto.Notebooks + (*NotebookCell)(nil), // 7: proto.NotebookCell + (*NotebookFileUploadRequest)(nil), // 8: proto.NotebookFileUploadRequest + (*NotebookFileUploadResponse)(nil), // 9: proto.NotebookFileUploadResponse + (*proto.ArtifactSpec)(nil), // 10: proto.ArtifactSpec + (*proto1.ArtifactParameter)(nil), // 11: proto.ArtifactParameter + (*proto2.VQLCollectorArgs)(nil), // 12: proto.VQLCollectorArgs + (*AvailableDownloads)(nil), // 13: proto.AvailableDownloads + (*proto1.ColumnType)(nil), // 14: proto.ColumnType } var file_notebooks_proto_depIdxs = []int32{ - 0, // 0: proto.NotebookCellRequest.env:type_name -> proto.Env - 3, // 1: proto.NotebookMetadata.context:type_name -> proto.NotebookContext - 6, // 2: proto.NotebookMetadata.cell_metadata:type_name -> proto.NotebookCell - 9, // 3: proto.NotebookMetadata.available_downloads:type_name -> proto.AvailableDownloads - 0, // 4: proto.NotebookMetadata.env:type_name -> proto.Env - 10, // 5: proto.NotebookMetadata.column_types:type_name -> proto.ColumnType - 4, // 6: proto.Notebooks.items:type_name -> proto.NotebookMetadata - 0, // 7: proto.NotebookCell.env:type_name -> proto.Env - 8, // [8:8] is the sub-list for method output_type - 8, // [8:8] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 1, // 0: proto.NotebookCellRequest.env:type_name -> proto.Env + 4, // 1: proto.NotebookMetadata.context:type_name -> proto.NotebookContext + 10, // 2: proto.NotebookMetadata.specs:type_name -> proto.ArtifactSpec + 11, // 3: proto.NotebookMetadata.parameters:type_name -> proto.ArtifactParameter + 12, // 4: proto.NotebookMetadata.requests:type_name -> proto.VQLCollectorArgs + 7, // 5: proto.NotebookMetadata.cell_metadata:type_name -> proto.NotebookCell + 13, // 6: proto.NotebookMetadata.available_downloads:type_name -> proto.AvailableDownloads + 13, // 7: proto.NotebookMetadata.available_uploads:type_name -> proto.AvailableDownloads + 1, // 8: proto.NotebookMetadata.env:type_name -> proto.Env + 14, // 9: proto.NotebookMetadata.column_types:type_name -> proto.ColumnType + 3, // 10: proto.NotebookMetadata.suggestions:type_name -> proto.NotebookCellRequest + 5, // 11: proto.Notebooks.items:type_name -> proto.NotebookMetadata + 1, // 12: proto.NotebookCell.env:type_name -> proto.Env + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_notebooks_proto_init() } @@ -939,7 +1314,7 @@ func file_notebooks_proto_init() { file_flows_proto_init() if !protoimpl.UnsafeEnabled { file_notebooks_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Env); i { + switch v := v.(*ReformatVQLMessage); i { case 0: return &v.state case 1: @@ -951,7 +1326,7 @@ func file_notebooks_proto_init() { } } file_notebooks_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NotebookExportRequest); i { + switch v := v.(*Env); i { case 0: return &v.state case 1: @@ -963,7 +1338,7 @@ func file_notebooks_proto_init() { } } file_notebooks_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NotebookCellRequest); i { + switch v := v.(*NotebookExportRequest); i { case 0: return &v.state case 1: @@ -975,7 +1350,7 @@ func file_notebooks_proto_init() { } } file_notebooks_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NotebookContext); i { + switch v := v.(*NotebookCellRequest); i { case 0: return &v.state case 1: @@ -987,7 +1362,7 @@ func file_notebooks_proto_init() { } } file_notebooks_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NotebookMetadata); i { + switch v := v.(*NotebookContext); i { case 0: return &v.state case 1: @@ -999,7 +1374,7 @@ func file_notebooks_proto_init() { } } file_notebooks_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Notebooks); i { + switch v := v.(*NotebookMetadata); i { case 0: return &v.state case 1: @@ -1011,7 +1386,7 @@ func file_notebooks_proto_init() { } } file_notebooks_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NotebookCell); i { + switch v := v.(*Notebooks); i { case 0: return &v.state case 1: @@ -1023,7 +1398,7 @@ func file_notebooks_proto_init() { } } file_notebooks_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NotebookFileUploadRequest); i { + switch v := v.(*NotebookCell); i { case 0: return &v.state case 1: @@ -1035,6 +1410,18 @@ func file_notebooks_proto_init() { } } file_notebooks_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NotebookFileUploadRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_notebooks_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*NotebookFileUploadResponse); i { case 0: return &v.state @@ -1053,7 +1440,7 @@ func file_notebooks_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_notebooks_proto_rawDesc, NumEnums: 0, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/api/proto/notebooks.proto b/api/proto/notebooks.proto index d8fdc1aba..509e6450e 100644 --- a/api/proto/notebooks.proto +++ b/api/proto/notebooks.proto @@ -1,6 +1,8 @@ syntax = "proto3"; +import "actions/proto/vql.proto"; import "artifacts/proto/artifact.proto"; +import "flows/proto/artifact_collector.proto"; import "flows.proto"; @@ -8,6 +10,11 @@ package proto; option go_package = "www.velocidex.com/golang/velociraptor/api/proto"; +message ReformatVQLMessage { + string vql = 1; + string artifact = 2; +} + message Env { string key = 1; string value = 2; @@ -16,12 +23,19 @@ message Env { message NotebookExportRequest { string notebook_id = 1; string type = 2; + string preferred_name = 3; } +// Message sent to the notebook processor ro request a cell recalc. message NotebookCellRequest { string notebook_id = 1; string cell_id = 2; + string version = 12; + repeated string available_versions = 13; + string input = 3; + string output = 14; + string name = 11; uint64 offset = 4; uint64 count = 5; @@ -30,6 +44,12 @@ message NotebookCellRequest { bool currently_editing = 8; repeated Env env = 9; + + bool include_uploads = 10; + bool include_timelines = 16; + + // If this is set schedule the calculation syncronously. + bool sync = 15; } message NotebookContext { @@ -37,8 +57,12 @@ message NotebookContext { string hunt_id = 2; string flow_id = 3; string client_id = 4; + string event_artifact = 5; + int64 start_time = 6; + int64 end_time = 7; } +// Represents an entire notebook. message NotebookMetadata { string name = 1; string description = 2; @@ -50,6 +74,21 @@ message NotebookMetadata { // A list of usernames that have access to this notebook. repeated string collaborators = 12; + // A list of NOTEBOOK artifacts to create the notebook with. + repeated string artifacts = 20; + + // Each notebook template can be passed parameters + repeated ArtifactSpec specs = 21; + + // Notebooks can have typed parameters which are injected into + // every cell. + repeated ArtifactParameter parameters = 22; + + // These queries will be run before each cell is evaluated in + // order to set up the parameters. + repeated VQLCollectorArgs requests = 23; + + // If this is set, the notebook is public. bool public = 13; @@ -69,6 +108,8 @@ message NotebookMetadata { AvailableDownloads available_downloads = 10; + AvailableDownloads available_uploads = 18; + // These environment variables will be populated into each // notebook cell in this notebook. repeated Env env = 14; @@ -76,6 +117,10 @@ message NotebookMetadata { repeated string timelines = 15; repeated ColumnType column_types = 17; + + // Cells that are not immediately included but may be included by + // the GUI as suggestions. + repeated NotebookCellRequest suggestions = 19; } message Notebooks { @@ -83,12 +128,20 @@ message Notebooks { } message NotebookCell { + string notebook_id = 16; string input = 1; string output = 2; + + // A short summary of the notebook cell. + string summary = 17; string data = 3; string cell_id = 4; repeated string messages = 5; + // True if there are more messages than are included in the + // messages field above. + bool more_messages = 12; + int64 timestamp = 6; int64 duration = 10; @@ -100,14 +153,24 @@ message NotebookCell { bool calculating = 9; repeated Env env = 11; + + string error = 13; + + string current_version = 14; + repeated string available_versions = 15; } message NotebookFileUploadRequest { string data = 1; string filename = 2; + bool disable_attachment_id = 6; + repeated string components = 4; string notebook_id = 3; + string cell_id = 5; } message NotebookFileUploadResponse { string url = 1; + string filename = 2; + string mime_type = 3; } diff --git a/api/proto/objects.pb.go b/api/proto/objects.pb.go index 39a21a8ed..2ac32f93d 100644 --- a/api/proto/objects.pb.go +++ b/api/proto/objects.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: objects.proto package proto diff --git a/api/proto/orgs.pb.go b/api/proto/orgs.pb.go new file mode 100644 index 000000000..37fde3367 --- /dev/null +++ b/api/proto/orgs.pb.go @@ -0,0 +1,169 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: orgs.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type OrgRecord struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Nonce string `protobuf:"bytes,2,opt,name=nonce,proto3" json:"nonce,omitempty"` + Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"` + // Deprecated do not use + OrgId string `protobuf:"bytes,4,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` +} + +func (x *OrgRecord) Reset() { + *x = OrgRecord{} + if protoimpl.UnsafeEnabled { + mi := &file_orgs_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OrgRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OrgRecord) ProtoMessage() {} + +func (x *OrgRecord) ProtoReflect() protoreflect.Message { + mi := &file_orgs_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OrgRecord.ProtoReflect.Descriptor instead. +func (*OrgRecord) Descriptor() ([]byte, []int) { + return file_orgs_proto_rawDescGZIP(), []int{0} +} + +func (x *OrgRecord) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *OrgRecord) GetNonce() string { + if x != nil { + return x.Nonce + } + return "" +} + +func (x *OrgRecord) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *OrgRecord) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +var File_orgs_proto protoreflect.FileDescriptor + +var file_orgs_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x6f, 0x72, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0x5c, 0x0a, 0x09, 0x4f, 0x72, 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, + 0x67, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, + 0x64, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, + 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, + 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_orgs_proto_rawDescOnce sync.Once + file_orgs_proto_rawDescData = file_orgs_proto_rawDesc +) + +func file_orgs_proto_rawDescGZIP() []byte { + file_orgs_proto_rawDescOnce.Do(func() { + file_orgs_proto_rawDescData = protoimpl.X.CompressGZIP(file_orgs_proto_rawDescData) + }) + return file_orgs_proto_rawDescData +} + +var file_orgs_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_orgs_proto_goTypes = []interface{}{ + (*OrgRecord)(nil), // 0: proto.OrgRecord +} +var file_orgs_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_orgs_proto_init() } +func file_orgs_proto_init() { + if File_orgs_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_orgs_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OrgRecord); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_orgs_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_orgs_proto_goTypes, + DependencyIndexes: file_orgs_proto_depIdxs, + MessageInfos: file_orgs_proto_msgTypes, + }.Build() + File_orgs_proto = out.File + file_orgs_proto_rawDesc = nil + file_orgs_proto_goTypes = nil + file_orgs_proto_depIdxs = nil +} diff --git a/api/proto/orgs.proto b/api/proto/orgs.proto new file mode 100644 index 000000000..7ef2dd0f6 --- /dev/null +++ b/api/proto/orgs.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package proto; + +option go_package = "www.velocidex.com/golang/velociraptor/api/proto"; + +message OrgRecord { + string name = 1; + string nonce = 2; + string id = 3; + + // Deprecated do not use + string org_id = 4; +} \ No newline at end of file diff --git a/api/proto/scheduler.pb.go b/api/proto/scheduler.pb.go new file mode 100644 index 000000000..1fc3f2e06 --- /dev/null +++ b/api/proto/scheduler.pb.go @@ -0,0 +1,296 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: scheduler.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ScheduleRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The queue we want to receive jobs on + Queue string `protobuf:"bytes,1,opt,name=queue,proto3" json:"queue,omitempty"` + // First request must be "register" then for each completed job "completion" + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + // The request ID this is a completion for. + Id uint64 `protobuf:"varint,3,opt,name=id,proto3" json:"id,omitempty"` + Priority int64 `protobuf:"varint,6,opt,name=priority,proto3" json:"priority,omitempty"` + // A json encoded response + Response string `protobuf:"bytes,4,opt,name=response,proto3" json:"response,omitempty"` + OrgId string `protobuf:"bytes,7,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + // An error message or "" for no error. + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *ScheduleRequest) Reset() { + *x = ScheduleRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_scheduler_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ScheduleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScheduleRequest) ProtoMessage() {} + +func (x *ScheduleRequest) ProtoReflect() protoreflect.Message { + mi := &file_scheduler_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScheduleRequest.ProtoReflect.Descriptor instead. +func (*ScheduleRequest) Descriptor() ([]byte, []int) { + return file_scheduler_proto_rawDescGZIP(), []int{0} +} + +func (x *ScheduleRequest) GetQueue() string { + if x != nil { + return x.Queue + } + return "" +} + +func (x *ScheduleRequest) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *ScheduleRequest) GetId() uint64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *ScheduleRequest) GetPriority() int64 { + if x != nil { + return x.Priority + } + return 0 +} + +func (x *ScheduleRequest) GetResponse() string { + if x != nil { + return x.Response + } + return "" +} + +func (x *ScheduleRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *ScheduleRequest) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// This represents a job request from the server to the minion, +type ScheduleResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The id must be matched with the response. + Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Queue string `protobuf:"bytes,2,opt,name=queue,proto3" json:"queue,omitempty"` + // A json serialized request suitable for the named queue. + Job string `protobuf:"bytes,3,opt,name=job,proto3" json:"job,omitempty"` + OrgId string `protobuf:"bytes,7,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` +} + +func (x *ScheduleResponse) Reset() { + *x = ScheduleResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_scheduler_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ScheduleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScheduleResponse) ProtoMessage() {} + +func (x *ScheduleResponse) ProtoReflect() protoreflect.Message { + mi := &file_scheduler_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScheduleResponse.ProtoReflect.Descriptor instead. +func (*ScheduleResponse) Descriptor() ([]byte, []int) { + return file_scheduler_proto_rawDescGZIP(), []int{1} +} + +func (x *ScheduleResponse) GetId() uint64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *ScheduleResponse) GetQueue() string { + if x != nil { + return x.Queue + } + return "" +} + +func (x *ScheduleResponse) GetJob() string { + if x != nil { + return x.Job + } + return "" +} + +func (x *ScheduleResponse) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +var File_scheduler_proto protoreflect.FileDescriptor + +var file_scheduler_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb0, 0x01, 0x0a, 0x0f, 0x53, 0x63, 0x68, + 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, + 0x71, 0x75, 0x65, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, + 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, + 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, + 0x74, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x15, + 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6f, 0x72, 0x67, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x61, 0x0a, 0x10, 0x53, + 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x71, 0x75, 0x65, 0x75, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6a, 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, + 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x42, 0x31, + 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, + 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_scheduler_proto_rawDescOnce sync.Once + file_scheduler_proto_rawDescData = file_scheduler_proto_rawDesc +) + +func file_scheduler_proto_rawDescGZIP() []byte { + file_scheduler_proto_rawDescOnce.Do(func() { + file_scheduler_proto_rawDescData = protoimpl.X.CompressGZIP(file_scheduler_proto_rawDescData) + }) + return file_scheduler_proto_rawDescData +} + +var file_scheduler_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_scheduler_proto_goTypes = []interface{}{ + (*ScheduleRequest)(nil), // 0: proto.ScheduleRequest + (*ScheduleResponse)(nil), // 1: proto.ScheduleResponse +} +var file_scheduler_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_scheduler_proto_init() } +func file_scheduler_proto_init() { + if File_scheduler_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_scheduler_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ScheduleRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_scheduler_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ScheduleResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_scheduler_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_scheduler_proto_goTypes, + DependencyIndexes: file_scheduler_proto_depIdxs, + MessageInfos: file_scheduler_proto_msgTypes, + }.Build() + File_scheduler_proto = out.File + file_scheduler_proto_rawDesc = nil + file_scheduler_proto_goTypes = nil + file_scheduler_proto_depIdxs = nil +} diff --git a/api/proto/scheduler.proto b/api/proto/scheduler.proto new file mode 100644 index 000000000..15c711828 --- /dev/null +++ b/api/proto/scheduler.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +package proto; + +option go_package = "www.velocidex.com/golang/velociraptor/api/proto"; + +message ScheduleRequest { + // The queue we want to receive jobs on + string queue = 1; + + // First request must be "register" then for each completed job "completion" + string type = 2; + + // The request ID this is a completion for. + uint64 id = 3; + + int64 priority = 6; + + // A json encoded response + string response = 4; + + string org_id = 7; + + // An error message or "" for no error. + string error = 5; +} + +// This represents a job request from the server to the minion, +message ScheduleResponse { + // The id must be matched with the response. + uint64 id = 1; + + string queue = 2; + + // A json serialized request suitable for the named queue. + string job = 3; + + string org_id = 7; +} \ No newline at end of file diff --git a/api/proto/secrets.pb.go b/api/proto/secrets.pb.go new file mode 100644 index 000000000..e7ad4ccd1 --- /dev/null +++ b/api/proto/secrets.pb.go @@ -0,0 +1,571 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: secrets.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SecretDefinition struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Verifier string `protobuf:"bytes,2,opt,name=verifier,proto3" json:"verifier,omitempty"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + // The fields that are supported by the secret. These are in a + // list to ensure consistent ordering. + Fields []string `protobuf:"bytes,7,rep,name=fields,proto3" json:"fields,omitempty"` + Template map[string]string `protobuf:"bytes,4,rep,name=template,proto3" json:"template,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + SecretNames []string `protobuf:"bytes,3,rep,name=secret_names,json=secretNames,proto3" json:"secret_names,omitempty"` + // deprecated - all secrets must be built in, no custom templates + // can be defined. + BuiltIn bool `protobuf:"varint,6,opt,name=built_in,json=builtIn,proto3" json:"built_in,omitempty"` + // These fields should be validated with yaml + YamlFields []string `protobuf:"bytes,8,rep,name=yaml_fields,json=yamlFields,proto3" json:"yaml_fields,omitempty"` +} + +func (x *SecretDefinition) Reset() { + *x = SecretDefinition{} + if protoimpl.UnsafeEnabled { + mi := &file_secrets_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SecretDefinition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretDefinition) ProtoMessage() {} + +func (x *SecretDefinition) ProtoReflect() protoreflect.Message { + mi := &file_secrets_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecretDefinition.ProtoReflect.Descriptor instead. +func (*SecretDefinition) Descriptor() ([]byte, []int) { + return file_secrets_proto_rawDescGZIP(), []int{0} +} + +func (x *SecretDefinition) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *SecretDefinition) GetVerifier() string { + if x != nil { + return x.Verifier + } + return "" +} + +func (x *SecretDefinition) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *SecretDefinition) GetFields() []string { + if x != nil { + return x.Fields + } + return nil +} + +func (x *SecretDefinition) GetTemplate() map[string]string { + if x != nil { + return x.Template + } + return nil +} + +func (x *SecretDefinition) GetSecretNames() []string { + if x != nil { + return x.SecretNames + } + return nil +} + +func (x *SecretDefinition) GetBuiltIn() bool { + if x != nil { + return x.BuiltIn + } + return false +} + +func (x *SecretDefinition) GetYamlFields() []string { + if x != nil { + return x.YamlFields + } + return nil +} + +type SecretDefinitionList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*SecretDefinition `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` +} + +func (x *SecretDefinitionList) Reset() { + *x = SecretDefinitionList{} + if protoimpl.UnsafeEnabled { + mi := &file_secrets_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SecretDefinitionList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretDefinitionList) ProtoMessage() {} + +func (x *SecretDefinitionList) ProtoReflect() protoreflect.Message { + mi := &file_secrets_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecretDefinitionList.ProtoReflect.Descriptor instead. +func (*SecretDefinitionList) Descriptor() ([]byte, []int) { + return file_secrets_proto_rawDescGZIP(), []int{1} +} + +func (x *SecretDefinitionList) GetItems() []*SecretDefinition { + if x != nil { + return x.Items + } + return nil +} + +type Secret struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + TypeName string `protobuf:"bytes,2,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + // The secret is stored as an encrypted json blob in storage. The + // blob is encrypted with the DEK derived from + // config_obj.Security.secrets_dek + EncryptedSecret []byte `protobuf:"bytes,6,opt,name=encrypted_secret,json=encryptedSecret,proto3" json:"encrypted_secret,omitempty"` + Secret map[string]string `protobuf:"bytes,3,rep,name=secret,proto3" json:"secret,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Users []string `protobuf:"bytes,4,rep,name=users,proto3" json:"users,omitempty"` + // A list of orgs which can see the secret. Only meaningful for + // secrets at the root org. + Orgs []string `protobuf:"bytes,7,rep,name=orgs,proto3" json:"orgs,omitempty"` + // When true this secret is available to all orgs automatically. + VisibleToAllOrgs bool `protobuf:"varint,8,opt,name=visible_to_all_orgs,json=visibleToAllOrgs,proto3" json:"visible_to_all_orgs,omitempty"` +} + +func (x *Secret) Reset() { + *x = Secret{} + if protoimpl.UnsafeEnabled { + mi := &file_secrets_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Secret) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Secret) ProtoMessage() {} + +func (x *Secret) ProtoReflect() protoreflect.Message { + mi := &file_secrets_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Secret.ProtoReflect.Descriptor instead. +func (*Secret) Descriptor() ([]byte, []int) { + return file_secrets_proto_rawDescGZIP(), []int{2} +} + +func (x *Secret) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Secret) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *Secret) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Secret) GetEncryptedSecret() []byte { + if x != nil { + return x.EncryptedSecret + } + return nil +} + +func (x *Secret) GetSecret() map[string]string { + if x != nil { + return x.Secret + } + return nil +} + +func (x *Secret) GetUsers() []string { + if x != nil { + return x.Users + } + return nil +} + +func (x *Secret) GetOrgs() []string { + if x != nil { + return x.Orgs + } + return nil +} + +func (x *Secret) GetVisibleToAllOrgs() bool { + if x != nil { + return x.VisibleToAllOrgs + } + return false +} + +type ModifySecretRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // If set the secret will be deleted. + Delete bool `protobuf:"varint,3,opt,name=delete,proto3" json:"delete,omitempty"` + AddUsers []string `protobuf:"bytes,4,rep,name=add_users,json=addUsers,proto3" json:"add_users,omitempty"` + RemoveUsers []string `protobuf:"bytes,5,rep,name=remove_users,json=removeUsers,proto3" json:"remove_users,omitempty"` + // Update the secret's org visibility list. Only meaningful for + // secrets at the root org. + AddOrgs []string `protobuf:"bytes,6,rep,name=add_orgs,json=addOrgs,proto3" json:"add_orgs,omitempty"` + RemoveOrgs []string `protobuf:"bytes,7,rep,name=remove_orgs,json=removeOrgs,proto3" json:"remove_orgs,omitempty"` + // When true this secret is available to all orgs automatically. + VisibleToAllOrgs bool `protobuf:"varint,8,opt,name=visible_to_all_orgs,json=visibleToAllOrgs,proto3" json:"visible_to_all_orgs,omitempty"` +} + +func (x *ModifySecretRequest) Reset() { + *x = ModifySecretRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_secrets_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ModifySecretRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModifySecretRequest) ProtoMessage() {} + +func (x *ModifySecretRequest) ProtoReflect() protoreflect.Message { + mi := &file_secrets_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModifySecretRequest.ProtoReflect.Descriptor instead. +func (*ModifySecretRequest) Descriptor() ([]byte, []int) { + return file_secrets_proto_rawDescGZIP(), []int{3} +} + +func (x *ModifySecretRequest) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *ModifySecretRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ModifySecretRequest) GetDelete() bool { + if x != nil { + return x.Delete + } + return false +} + +func (x *ModifySecretRequest) GetAddUsers() []string { + if x != nil { + return x.AddUsers + } + return nil +} + +func (x *ModifySecretRequest) GetRemoveUsers() []string { + if x != nil { + return x.RemoveUsers + } + return nil +} + +func (x *ModifySecretRequest) GetAddOrgs() []string { + if x != nil { + return x.AddOrgs + } + return nil +} + +func (x *ModifySecretRequest) GetRemoveOrgs() []string { + if x != nil { + return x.RemoveOrgs + } + return nil +} + +func (x *ModifySecretRequest) GetVisibleToAllOrgs() bool { + if x != nil { + return x.VisibleToAllOrgs + } + return false +} + +var File_secrets_proto protoreflect.FileDescriptor + +var file_secrets_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe4, 0x02, 0x0a, 0x10, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x74, + 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x65, 0x72, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x65, 0x72, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, + 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x41, + 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x25, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x44, + 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, + 0x74, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x5f, 0x69, 0x6e, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x49, 0x6e, 0x12, + 0x1f, 0x0a, 0x0b, 0x79, 0x61, 0x6d, 0x6c, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x08, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x79, 0x61, 0x6d, 0x6c, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, + 0x1a, 0x3b, 0x0a, 0x0d, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x45, 0x0a, + 0x14, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x22, 0xcd, 0x02, 0x0a, 0x06, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x65, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x31, 0x0a, + 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2e, 0x53, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6f, 0x72, 0x67, 0x73, 0x18, 0x07, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6f, 0x72, 0x67, 0x73, 0x12, 0x2d, 0x0a, 0x13, 0x76, 0x69, + 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x61, 0x6c, 0x6c, 0x5f, 0x6f, 0x72, 0x67, + 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, + 0x54, 0x6f, 0x41, 0x6c, 0x6c, 0x4f, 0x72, 0x67, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x53, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0x89, 0x02, 0x0a, 0x13, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x5f, 0x75, 0x73, 0x65, + 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x64, 0x55, 0x73, 0x65, + 0x72, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x5f, 0x75, 0x73, 0x65, + 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x64, 0x64, 0x5f, 0x6f, 0x72, 0x67, + 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x4f, 0x72, 0x67, 0x73, + 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x5f, 0x6f, 0x72, 0x67, 0x73, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x72, 0x67, + 0x73, 0x12, 0x2d, 0x0a, 0x13, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x74, 0x6f, 0x5f, + 0x61, 0x6c, 0x6c, 0x5f, 0x6f, 0x72, 0x67, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, + 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x54, 0x6f, 0x41, 0x6c, 0x6c, 0x4f, 0x72, 0x67, 0x73, + 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, + 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, + 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_secrets_proto_rawDescOnce sync.Once + file_secrets_proto_rawDescData = file_secrets_proto_rawDesc +) + +func file_secrets_proto_rawDescGZIP() []byte { + file_secrets_proto_rawDescOnce.Do(func() { + file_secrets_proto_rawDescData = protoimpl.X.CompressGZIP(file_secrets_proto_rawDescData) + }) + return file_secrets_proto_rawDescData +} + +var file_secrets_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_secrets_proto_goTypes = []interface{}{ + (*SecretDefinition)(nil), // 0: proto.SecretDefinition + (*SecretDefinitionList)(nil), // 1: proto.SecretDefinitionList + (*Secret)(nil), // 2: proto.Secret + (*ModifySecretRequest)(nil), // 3: proto.ModifySecretRequest + nil, // 4: proto.SecretDefinition.TemplateEntry + nil, // 5: proto.Secret.SecretEntry +} +var file_secrets_proto_depIdxs = []int32{ + 4, // 0: proto.SecretDefinition.template:type_name -> proto.SecretDefinition.TemplateEntry + 0, // 1: proto.SecretDefinitionList.items:type_name -> proto.SecretDefinition + 5, // 2: proto.Secret.secret:type_name -> proto.Secret.SecretEntry + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_secrets_proto_init() } +func file_secrets_proto_init() { + if File_secrets_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_secrets_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SecretDefinition); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_secrets_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SecretDefinitionList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_secrets_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Secret); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_secrets_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ModifySecretRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_secrets_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_secrets_proto_goTypes, + DependencyIndexes: file_secrets_proto_depIdxs, + MessageInfos: file_secrets_proto_msgTypes, + }.Build() + File_secrets_proto = out.File + file_secrets_proto_rawDesc = nil + file_secrets_proto_goTypes = nil + file_secrets_proto_depIdxs = nil +} diff --git a/api/proto/secrets.proto b/api/proto/secrets.proto new file mode 100644 index 000000000..4eb3717fc --- /dev/null +++ b/api/proto/secrets.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; + +package proto; + +option go_package = "www.velocidex.com/golang/velociraptor/api/proto"; + +message SecretDefinition { + string type_name = 1; + string verifier = 2; + string description = 5; + + // The fields that are supported by the secret. These are in a + // list to ensure consistent ordering. + repeated string fields = 7; + + map template = 4; + repeated string secret_names = 3; + + // deprecated - all secrets must be built in, no custom templates + // can be defined. + bool built_in = 6; + + // These fields should be validated with yaml + repeated string yaml_fields = 8; +} + +message SecretDefinitionList { + repeated SecretDefinition items = 1; +} + +message Secret { + string name = 1; + string type_name = 2; + string description = 5; + + // The secret is stored as an encrypted json blob in storage. The + // blob is encrypted with the DEK derived from + // config_obj.Security.secrets_dek + bytes encrypted_secret = 6; + + map secret = 3; + + repeated string users = 4; + + // A list of orgs which can see the secret. Only meaningful for + // secrets at the root org. + repeated string orgs = 7; + + // When true this secret is available to all orgs automatically. + bool visible_to_all_orgs = 8; +} + +message ModifySecretRequest { + string type_name = 1; + string name = 2; + + // If set the secret will be deleted. + bool delete = 3; + repeated string add_users = 4; + repeated string remove_users = 5; + + // Update the secret's org visibility list. Only meaningful for + // secrets at the root org. + repeated string add_orgs = 6; + repeated string remove_orgs = 7; + + // When true this secret is available to all orgs automatically. + bool visible_to_all_orgs = 8; +} diff --git a/api/proto/server_state.pb.go b/api/proto/server_state.pb.go index 5f174ce73..82a8fe89f 100644 --- a/api/proto/server_state.pb.go +++ b/api/proto/server_state.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: server_state.proto package proto diff --git a/api/proto/timeline_api.pb.go b/api/proto/timeline_api.pb.go new file mode 100644 index 000000000..95d6a1184 --- /dev/null +++ b/api/proto/timeline_api.pb.go @@ -0,0 +1,182 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: timeline_api.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AnnotationRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + SuperTimeline string `protobuf:"bytes,2,opt,name=super_timeline,json=superTimeline,proto3" json:"super_timeline,omitempty"` + NotebookId string `protobuf:"bytes,3,opt,name=notebook_id,json=notebookId,proto3" json:"notebook_id,omitempty"` + Note string `protobuf:"bytes,4,opt,name=note,proto3" json:"note,omitempty"` + EventJson string `protobuf:"bytes,5,opt,name=event_json,json=eventJson,proto3" json:"event_json,omitempty"` +} + +func (x *AnnotationRequest) Reset() { + *x = AnnotationRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_timeline_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AnnotationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AnnotationRequest) ProtoMessage() {} + +func (x *AnnotationRequest) ProtoReflect() protoreflect.Message { + mi := &file_timeline_api_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AnnotationRequest.ProtoReflect.Descriptor instead. +func (*AnnotationRequest) Descriptor() ([]byte, []int) { + return file_timeline_api_proto_rawDescGZIP(), []int{0} +} + +func (x *AnnotationRequest) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *AnnotationRequest) GetSuperTimeline() string { + if x != nil { + return x.SuperTimeline + } + return "" +} + +func (x *AnnotationRequest) GetNotebookId() string { + if x != nil { + return x.NotebookId + } + return "" +} + +func (x *AnnotationRequest) GetNote() string { + if x != nil { + return x.Note + } + return "" +} + +func (x *AnnotationRequest) GetEventJson() string { + if x != nil { + return x.EventJson + } + return "" +} + +var File_timeline_api_proto protoreflect.FileDescriptor + +var file_timeline_api_proto_rawDesc = []byte{ + 0x0a, 0x12, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x70, 0x69, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xac, 0x01, 0x0a, 0x11, + 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, + 0x25, 0x0a, 0x0e, 0x73, 0x75, 0x70, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x75, 0x70, 0x65, 0x72, 0x54, 0x69, + 0x6d, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x74, 0x65, 0x62, 0x6f, + 0x6f, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x6f, 0x74, + 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x65, + 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4a, 0x73, 0x6f, 0x6e, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, + 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, + 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_timeline_api_proto_rawDescOnce sync.Once + file_timeline_api_proto_rawDescData = file_timeline_api_proto_rawDesc +) + +func file_timeline_api_proto_rawDescGZIP() []byte { + file_timeline_api_proto_rawDescOnce.Do(func() { + file_timeline_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_timeline_api_proto_rawDescData) + }) + return file_timeline_api_proto_rawDescData +} + +var file_timeline_api_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_timeline_api_proto_goTypes = []interface{}{ + (*AnnotationRequest)(nil), // 0: proto.AnnotationRequest +} +var file_timeline_api_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_timeline_api_proto_init() } +func file_timeline_api_proto_init() { + if File_timeline_api_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_timeline_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AnnotationRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_timeline_api_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_timeline_api_proto_goTypes, + DependencyIndexes: file_timeline_api_proto_depIdxs, + MessageInfos: file_timeline_api_proto_msgTypes, + }.Build() + File_timeline_api_proto = out.File + file_timeline_api_proto_rawDesc = nil + file_timeline_api_proto_goTypes = nil + file_timeline_api_proto_depIdxs = nil +} diff --git a/api/proto/timeline_api.proto b/api/proto/timeline_api.proto new file mode 100644 index 000000000..550b4fbc4 --- /dev/null +++ b/api/proto/timeline_api.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package proto; + +option go_package = "www.velocidex.com/golang/velociraptor/api/proto"; + +message AnnotationRequest { + int64 timestamp = 1; + string super_timeline = 2; + string notebook_id = 3; + string note = 4; + string event_json = 5; +} \ No newline at end of file diff --git a/api/proto/users.pb.go b/api/proto/users.pb.go index 2918b5c78..92c9121e7 100644 --- a/api/proto/users.pb.go +++ b/api/proto/users.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: users.proto package proto @@ -12,7 +9,8 @@ import ( reflect "reflect" sync "sync" proto "www.velocidex.com/golang/velociraptor/acls/proto" - proto1 "www.velocidex.com/golang/velociraptor/flows/proto" + proto1 "www.velocidex.com/golang/velociraptor/config/proto" + proto2 "www.velocidex.com/golang/velociraptor/flows/proto" _ "www.velocidex.com/golang/velociraptor/proto" ) @@ -23,242 +21,156 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type GUISettings_UIMode int32 +type ApiUser_UserType int32 const ( - GUISettings_BASIC GUISettings_UIMode = 0 - GUISettings_ADVANCED GUISettings_UIMode = 1 - GUISettings_DEBUG GUISettings_UIMode = 2 + ApiUser_USER_TYPE_NONE ApiUser_UserType = 0 + ApiUser_USER_TYPE_STANDARD ApiUser_UserType = 1 + ApiUser_USER_TYPE_ADMIN ApiUser_UserType = 2 ) -// Enum value maps for GUISettings_UIMode. +// Enum value maps for ApiUser_UserType. var ( - GUISettings_UIMode_name = map[int32]string{ - 0: "BASIC", - 1: "ADVANCED", - 2: "DEBUG", - } - GUISettings_UIMode_value = map[string]int32{ - "BASIC": 0, - "ADVANCED": 1, - "DEBUG": 2, - } -) - -func (x GUISettings_UIMode) Enum() *GUISettings_UIMode { - p := new(GUISettings_UIMode) - *p = x - return p -} - -func (x GUISettings_UIMode) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (GUISettings_UIMode) Descriptor() protoreflect.EnumDescriptor { - return file_users_proto_enumTypes[0].Descriptor() -} - -func (GUISettings_UIMode) Type() protoreflect.EnumType { - return &file_users_proto_enumTypes[0] -} - -func (x GUISettings_UIMode) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use GUISettings_UIMode.Descriptor instead. -func (GUISettings_UIMode) EnumDescriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{1, 0} -} - -type ApiGrrUser_UserType int32 - -const ( - ApiGrrUser_USER_TYPE_NONE ApiGrrUser_UserType = 0 - ApiGrrUser_USER_TYPE_STANDARD ApiGrrUser_UserType = 1 - ApiGrrUser_USER_TYPE_ADMIN ApiGrrUser_UserType = 2 -) - -// Enum value maps for ApiGrrUser_UserType. -var ( - ApiGrrUser_UserType_name = map[int32]string{ + ApiUser_UserType_name = map[int32]string{ 0: "USER_TYPE_NONE", 1: "USER_TYPE_STANDARD", 2: "USER_TYPE_ADMIN", } - ApiGrrUser_UserType_value = map[string]int32{ + ApiUser_UserType_value = map[string]int32{ "USER_TYPE_NONE": 0, "USER_TYPE_STANDARD": 1, "USER_TYPE_ADMIN": 2, } ) -func (x ApiGrrUser_UserType) Enum() *ApiGrrUser_UserType { - p := new(ApiGrrUser_UserType) +func (x ApiUser_UserType) Enum() *ApiUser_UserType { + p := new(ApiUser_UserType) *p = x return p } -func (x ApiGrrUser_UserType) String() string { +func (x ApiUser_UserType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } -func (ApiGrrUser_UserType) Descriptor() protoreflect.EnumDescriptor { - return file_users_proto_enumTypes[1].Descriptor() +func (ApiUser_UserType) Descriptor() protoreflect.EnumDescriptor { + return file_users_proto_enumTypes[0].Descriptor() } -func (ApiGrrUser_UserType) Type() protoreflect.EnumType { - return &file_users_proto_enumTypes[1] +func (ApiUser_UserType) Type() protoreflect.EnumType { + return &file_users_proto_enumTypes[0] } -func (x ApiGrrUser_UserType) Number() protoreflect.EnumNumber { +func (x ApiUser_UserType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } -// Deprecated: Use ApiGrrUser_UserType.Descriptor instead. -func (ApiGrrUser_UserType) EnumDescriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{4, 0} +// Deprecated: Use ApiUser_UserType.Descriptor instead. +func (ApiUser_UserType) EnumDescriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{7, 0} } -type UserNotification_Type int32 +type Strings struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields -const ( - UserNotification_TYPE_UNSET UserNotification_Type = 0 - UserNotification_TYPE_CLIENT_INTERROGATED UserNotification_Type = 1 - UserNotification_TYPE_CLIENT_APPROVAL_REQUESTED UserNotification_Type = 2 - UserNotification_TYPE_HUNT_APPROVAL_REQUESTED UserNotification_Type = 3 - UserNotification_TYPE_CRON_JOB_APPROVAL_REQUESTED UserNotification_Type = 4 - UserNotification_TYPE_CLIENT_APPROVAL_GRANTED UserNotification_Type = 5 - UserNotification_TYPE_HUNT_APPROVAL_GRANTED UserNotification_Type = 6 - UserNotification_TYPE_CRON_JOB_APPROVAL_GRANTED UserNotification_Type = 7 - UserNotification_TYPE_VFS_FILE_COLLECTED UserNotification_Type = 8 - UserNotification_TYPE_VFS_FILE_COLLECTION_FAILED UserNotification_Type = 9 - UserNotification_TYPE_HUNT_STOPPED UserNotification_Type = 10 - UserNotification_TYPE_FILE_ARCHIVE_GENERATED UserNotification_Type = 11 - UserNotification_TYPE_FILE_ARCHIVE_GENERATION_FAILED UserNotification_Type = 12 - UserNotification_TYPE_FLOW_RUN_COMPLETED UserNotification_Type = 13 - UserNotification_TYPE_FLOW_RUN_FAILED UserNotification_Type = 14 - UserNotification_TYPE_VFS_LIST_DIRECTORY_COMPLETED UserNotification_Type = 15 - UserNotification_TYPE_VFS_RECURSIVE_LIST_DIRECTORY_COMPLETED UserNotification_Type = 16 -) + Strings []string `protobuf:"bytes,1,rep,name=strings,proto3" json:"strings,omitempty"` +} -// Enum value maps for UserNotification_Type. -var ( - UserNotification_Type_name = map[int32]string{ - 0: "TYPE_UNSET", - 1: "TYPE_CLIENT_INTERROGATED", - 2: "TYPE_CLIENT_APPROVAL_REQUESTED", - 3: "TYPE_HUNT_APPROVAL_REQUESTED", - 4: "TYPE_CRON_JOB_APPROVAL_REQUESTED", - 5: "TYPE_CLIENT_APPROVAL_GRANTED", - 6: "TYPE_HUNT_APPROVAL_GRANTED", - 7: "TYPE_CRON_JOB_APPROVAL_GRANTED", - 8: "TYPE_VFS_FILE_COLLECTED", - 9: "TYPE_VFS_FILE_COLLECTION_FAILED", - 10: "TYPE_HUNT_STOPPED", - 11: "TYPE_FILE_ARCHIVE_GENERATED", - 12: "TYPE_FILE_ARCHIVE_GENERATION_FAILED", - 13: "TYPE_FLOW_RUN_COMPLETED", - 14: "TYPE_FLOW_RUN_FAILED", - 15: "TYPE_VFS_LIST_DIRECTORY_COMPLETED", - 16: "TYPE_VFS_RECURSIVE_LIST_DIRECTORY_COMPLETED", - } - UserNotification_Type_value = map[string]int32{ - "TYPE_UNSET": 0, - "TYPE_CLIENT_INTERROGATED": 1, - "TYPE_CLIENT_APPROVAL_REQUESTED": 2, - "TYPE_HUNT_APPROVAL_REQUESTED": 3, - "TYPE_CRON_JOB_APPROVAL_REQUESTED": 4, - "TYPE_CLIENT_APPROVAL_GRANTED": 5, - "TYPE_HUNT_APPROVAL_GRANTED": 6, - "TYPE_CRON_JOB_APPROVAL_GRANTED": 7, - "TYPE_VFS_FILE_COLLECTED": 8, - "TYPE_VFS_FILE_COLLECTION_FAILED": 9, - "TYPE_HUNT_STOPPED": 10, - "TYPE_FILE_ARCHIVE_GENERATED": 11, - "TYPE_FILE_ARCHIVE_GENERATION_FAILED": 12, - "TYPE_FLOW_RUN_COMPLETED": 13, - "TYPE_FLOW_RUN_FAILED": 14, - "TYPE_VFS_LIST_DIRECTORY_COMPLETED": 15, - "TYPE_VFS_RECURSIVE_LIST_DIRECTORY_COMPLETED": 16, +func (x *Strings) Reset() { + *x = Strings{} + if protoimpl.UnsafeEnabled { + mi := &file_users_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -) - -func (x UserNotification_Type) Enum() *UserNotification_Type { - p := new(UserNotification_Type) - *p = x - return p } -func (x UserNotification_Type) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +func (x *Strings) String() string { + return protoimpl.X.MessageStringOf(x) } -func (UserNotification_Type) Descriptor() protoreflect.EnumDescriptor { - return file_users_proto_enumTypes[2].Descriptor() -} +func (*Strings) ProtoMessage() {} -func (UserNotification_Type) Type() protoreflect.EnumType { - return &file_users_proto_enumTypes[2] +func (x *Strings) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -func (x UserNotification_Type) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) +// Deprecated: Use Strings.ProtoReflect.Descriptor instead. +func (*Strings) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{0} } -// Deprecated: Use UserNotification_Type.Descriptor instead. -func (UserNotification_Type) EnumDescriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{6, 0} +func (x *Strings) GetStrings() []string { + if x != nil { + return x.Strings + } + return nil } -type UserNotification_State int32 +type UserStats struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields -const ( - UserNotification_STATE_UNSET UserNotification_State = 0 - UserNotification_STATE_PENDING UserNotification_State = 1 - UserNotification_STATE_NOT_PENDING UserNotification_State = 2 -) + // Last time the user's record was fetched from the cache. + LastActiveTime int64 `protobuf:"varint,1,opt,name=last_active_time,json=lastActiveTime,proto3" json:"last_active_time,omitempty"` + LastIpAddress string `protobuf:"bytes,2,opt,name=last_ip_address,json=lastIpAddress,proto3" json:"last_ip_address,omitempty"` +} -// Enum value maps for UserNotification_State. -var ( - UserNotification_State_name = map[int32]string{ - 0: "STATE_UNSET", - 1: "STATE_PENDING", - 2: "STATE_NOT_PENDING", - } - UserNotification_State_value = map[string]int32{ - "STATE_UNSET": 0, - "STATE_PENDING": 1, - "STATE_NOT_PENDING": 2, +func (x *UserStats) Reset() { + *x = UserStats{} + if protoimpl.UnsafeEnabled { + mi := &file_users_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -) - -func (x UserNotification_State) Enum() *UserNotification_State { - p := new(UserNotification_State) - *p = x - return p } -func (x UserNotification_State) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +func (x *UserStats) String() string { + return protoimpl.X.MessageStringOf(x) } -func (UserNotification_State) Descriptor() protoreflect.EnumDescriptor { - return file_users_proto_enumTypes[3].Descriptor() +func (*UserStats) ProtoMessage() {} + +func (x *UserStats) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -func (UserNotification_State) Type() protoreflect.EnumType { - return &file_users_proto_enumTypes[3] +// Deprecated: Use UserStats.ProtoReflect.Descriptor instead. +func (*UserStats) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{1} } -func (x UserNotification_State) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) +func (x *UserStats) GetLastActiveTime() int64 { + if x != nil { + return x.LastActiveTime + } + return 0 } -// Deprecated: Use UserNotification_State.Descriptor instead. -func (UserNotification_State) EnumDescriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{6, 1} +func (x *UserStats) GetLastIpAddress() string { + if x != nil { + return x.LastIpAddress + } + return "" } type VelociraptorUser struct { @@ -275,12 +187,18 @@ type VelociraptorUser struct { ReadOnly bool `protobuf:"varint,7,opt,name=read_only,json=readOnly,proto3" json:"read_only,omitempty"` Locked bool `protobuf:"varint,8,opt,name=locked,proto3" json:"locked,omitempty"` Permissions *proto.ApiClientACL `protobuf:"bytes,9,opt,name=Permissions,proto3" json:"Permissions,omitempty"` + // A list of org id's the user belongs to. + Orgs []*OrgRecord `protobuf:"bytes,11,rep,name=orgs,proto3" json:"orgs,omitempty"` + // Only used by the GUI/API to determine the currently selected + // org the user wants to see. + CurrentOrg string `protobuf:"bytes,12,opt,name=current_org,json=currentOrg,proto3" json:"current_org,omitempty"` + Stats *UserStats `protobuf:"bytes,13,opt,name=stats,proto3" json:"stats,omitempty"` } func (x *VelociraptorUser) Reset() { *x = VelociraptorUser{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[0] + mi := &file_users_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -293,7 +211,7 @@ func (x *VelociraptorUser) String() string { func (*VelociraptorUser) ProtoMessage() {} func (x *VelociraptorUser) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[0] + mi := &file_users_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -306,7 +224,7 @@ func (x *VelociraptorUser) ProtoReflect() protoreflect.Message { // Deprecated: Use VelociraptorUser.ProtoReflect.Descriptor instead. func (*VelociraptorUser) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{0} + return file_users_proto_rawDescGZIP(), []int{2} } func (x *VelociraptorUser) GetName() string { @@ -372,33 +290,56 @@ func (x *VelociraptorUser) GetPermissions() *proto.ApiClientACL { return nil } -// Next field: 4 -type GUISettings struct { +func (x *VelociraptorUser) GetOrgs() []*OrgRecord { + if x != nil { + return x.Orgs + } + return nil +} + +func (x *VelociraptorUser) GetCurrentOrg() string { + if x != nil { + return x.CurrentOrg + } + return "" +} + +func (x *VelociraptorUser) GetStats() *UserStats { + if x != nil { + return x.Stats + } + return nil +} + +type UpdateUserRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Mode GUISettings_UIMode `protobuf:"varint,1,opt,name=mode,proto3,enum=proto.GUISettings_UIMode" json:"mode,omitempty"` - CanaryMode bool `protobuf:"varint,3,opt,name=canary_mode,json=canaryMode,proto3" json:"canary_mode,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + Orgs []string `protobuf:"bytes,3,rep,name=orgs,proto3" json:"orgs,omitempty"` + Roles []string `protobuf:"bytes,4,rep,name=roles,proto3" json:"roles,omitempty"` + AddNewUser bool `protobuf:"varint,5,opt,name=add_new_user,json=addNewUser,proto3" json:"add_new_user,omitempty"` } -func (x *GUISettings) Reset() { - *x = GUISettings{} +func (x *UpdateUserRequest) Reset() { + *x = UpdateUserRequest{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[1] + mi := &file_users_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *GUISettings) String() string { +func (x *UpdateUserRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GUISettings) ProtoMessage() {} +func (*UpdateUserRequest) ProtoMessage() {} -func (x *GUISettings) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[1] +func (x *UpdateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -409,51 +350,72 @@ func (x *GUISettings) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GUISettings.ProtoReflect.Descriptor instead. -func (*GUISettings) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{1} +// Deprecated: Use UpdateUserRequest.ProtoReflect.Descriptor instead. +func (*UpdateUserRequest) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{3} } -func (x *GUISettings) GetMode() GUISettings_UIMode { +func (x *UpdateUserRequest) GetName() string { if x != nil { - return x.Mode + return x.Name } - return GUISettings_BASIC + return "" } -func (x *GUISettings) GetCanaryMode() bool { +func (x *UpdateUserRequest) GetPassword() string { if x != nil { - return x.CanaryMode + return x.Password + } + return "" +} + +func (x *UpdateUserRequest) GetOrgs() []string { + if x != nil { + return x.Orgs + } + return nil +} + +func (x *UpdateUserRequest) GetRoles() []string { + if x != nil { + return x.Roles + } + return nil +} + +func (x *UpdateUserRequest) GetAddNewUser() bool { + if x != nil { + return x.AddNewUser } return false } -type UILink struct { +type DeleteUserRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` - Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Orgs []string `protobuf:"bytes,2,rep,name=orgs,proto3" json:"orgs,omitempty"` } -func (x *UILink) Reset() { - *x = UILink{} +func (x *DeleteUserRequest) Reset() { + *x = DeleteUserRequest{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[2] + mi := &file_users_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *UILink) String() string { +func (x *DeleteUserRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*UILink) ProtoMessage() {} +func (*DeleteUserRequest) ProtoMessage() {} -func (x *UILink) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[2] +func (x *DeleteUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -464,63 +426,134 @@ func (x *UILink) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use UILink.ProtoReflect.Descriptor instead. -func (*UILink) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{2} +// Deprecated: Use DeleteUserRequest.ProtoReflect.Descriptor instead. +func (*DeleteUserRequest) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{4} +} + +func (x *DeleteUserRequest) GetName() string { + if x != nil { + return x.Name + } + return "" } -func (x *UILink) GetText() string { +func (x *DeleteUserRequest) GetOrgs() []string { if x != nil { - return x.Text + return x.Orgs + } + return nil +} + +type UserRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Org string `protobuf:"bytes,2,opt,name=org,proto3" json:"org,omitempty"` +} + +func (x *UserRequest) Reset() { + *x = UserRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_users_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserRequest) ProtoMessage() {} + +func (x *UserRequest) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserRequest.ProtoReflect.Descriptor instead. +func (*UserRequest) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{5} +} + +func (x *UserRequest) GetName() string { + if x != nil { + return x.Name } return "" } -func (x *UILink) GetUrl() string { +func (x *UserRequest) GetOrg() string { if x != nil { - return x.Url + return x.Org } return "" } -// These traits are used by the AdminUI Angular app to disable certain -// UI elements based on the user's permission set. -type ApiGrrUserInterfaceTraits struct { +// These traits are used to control the GUI App. Many of these fields +// are constructed from the VelociraptorUser, the config file, the +// GUIOptions etc. +type ApiUserInterfaceTraits struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Permissions *proto.ApiClientACL `protobuf:"bytes,9,opt,name=Permissions,proto3" json:"Permissions,omitempty"` - AuthUsingGoogle bool `protobuf:"varint,19,opt,name=auth_using_google,json=authUsingGoogle,proto3" json:"auth_using_google,omitempty"` - Picture string `protobuf:"bytes,20,opt,name=picture,proto3" json:"picture,omitempty"` - Links []*UILink `protobuf:"bytes,21,rep,name=links,proto3" json:"links,omitempty"` + Permissions *proto.ApiClientACL `protobuf:"bytes,9,opt,name=Permissions,proto3" json:"Permissions,omitempty"` + Customizations *GUICustomizations `protobuf:"bytes,10,opt,name=customizations,proto3" json:"customizations,omitempty"` + Lang string `protobuf:"bytes,22,opt,name=lang,proto3" json:"lang,omitempty"` + // Set if the authenticator is password less (e.g. OAuth, SAML + // etc) + PasswordLess bool `protobuf:"varint,25,opt,name=password_less,json=passwordLess,proto3" json:"password_less,omitempty"` + Picture string `protobuf:"bytes,20,opt,name=picture,proto3" json:"picture,omitempty"` + Links []*proto1.GUILink `protobuf:"bytes,21,rep,name=links,proto3" json:"links,omitempty"` // Get the user's preferred theme. - Theme string `protobuf:"bytes,2,opt,name=theme,proto3" json:"theme,omitempty"` + Theme string `protobuf:"bytes,2,opt,name=theme,proto3" json:"theme,omitempty"` + Timezone string `protobuf:"bytes,23,opt,name=timezone,proto3" json:"timezone,omitempty"` // Downloads will be protected using this password. DefaultPassword string `protobuf:"bytes,3,opt,name=default_password,json=defaultPassword,proto3" json:"default_password,omitempty"` // Offer to protect download exports by default. DefaultDownloadsLock bool `protobuf:"varint,4,opt,name=default_downloads_lock,json=defaultDownloadsLock,proto3" json:"default_downloads_lock,omitempty"` // An opaque setting object stored by the GUI. UiSettings string `protobuf:"bytes,1,opt,name=ui_settings,json=uiSettings,proto3" json:"ui_settings,omitempty"` -} - -func (x *ApiGrrUserInterfaceTraits) Reset() { - *x = ApiGrrUserInterfaceTraits{} + // Current selected Org. + Org string `protobuf:"bytes,24,opt,name=org,proto3" json:"org,omitempty"` + OrgName string `protobuf:"bytes,30,opt,name=org_name,json=orgName,proto3" json:"org_name,omitempty"` + // The user's specific base path - only used for crazy reverse + // proxy configurations. + BasePath string `protobuf:"bytes,29,opt,name=base_path,json=basePath,proto3" json:"base_path,omitempty"` + // Optional features of the UI to disable. + DisableServerEvents bool `protobuf:"varint,26,opt,name=disable_server_events,json=disableServerEvents,proto3" json:"disable_server_events,omitempty"` + AuthRedirectTemplate string `protobuf:"bytes,27,opt,name=auth_redirect_template,json=authRedirectTemplate,proto3" json:"auth_redirect_template,omitempty"` + DisableQuarantineButton bool `protobuf:"varint,28,opt,name=disable_quarantine_button,json=disableQuarantineButton,proto3" json:"disable_quarantine_button,omitempty"` +} + +func (x *ApiUserInterfaceTraits) Reset() { + *x = ApiUserInterfaceTraits{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[3] + mi := &file_users_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *ApiGrrUserInterfaceTraits) String() string { +func (x *ApiUserInterfaceTraits) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ApiGrrUserInterfaceTraits) ProtoMessage() {} +func (*ApiUserInterfaceTraits) ProtoMessage() {} -func (x *ApiGrrUserInterfaceTraits) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[3] +func (x *ApiUserInterfaceTraits) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -531,95 +564,163 @@ func (x *ApiGrrUserInterfaceTraits) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ApiGrrUserInterfaceTraits.ProtoReflect.Descriptor instead. -func (*ApiGrrUserInterfaceTraits) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{3} +// Deprecated: Use ApiUserInterfaceTraits.ProtoReflect.Descriptor instead. +func (*ApiUserInterfaceTraits) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{6} } -func (x *ApiGrrUserInterfaceTraits) GetPermissions() *proto.ApiClientACL { +func (x *ApiUserInterfaceTraits) GetPermissions() *proto.ApiClientACL { if x != nil { return x.Permissions } return nil } -func (x *ApiGrrUserInterfaceTraits) GetAuthUsingGoogle() bool { +func (x *ApiUserInterfaceTraits) GetCustomizations() *GUICustomizations { + if x != nil { + return x.Customizations + } + return nil +} + +func (x *ApiUserInterfaceTraits) GetLang() string { + if x != nil { + return x.Lang + } + return "" +} + +func (x *ApiUserInterfaceTraits) GetPasswordLess() bool { if x != nil { - return x.AuthUsingGoogle + return x.PasswordLess } return false } -func (x *ApiGrrUserInterfaceTraits) GetPicture() string { +func (x *ApiUserInterfaceTraits) GetPicture() string { if x != nil { return x.Picture } return "" } -func (x *ApiGrrUserInterfaceTraits) GetLinks() []*UILink { +func (x *ApiUserInterfaceTraits) GetLinks() []*proto1.GUILink { if x != nil { return x.Links } return nil } -func (x *ApiGrrUserInterfaceTraits) GetTheme() string { +func (x *ApiUserInterfaceTraits) GetTheme() string { if x != nil { return x.Theme } return "" } -func (x *ApiGrrUserInterfaceTraits) GetDefaultPassword() string { +func (x *ApiUserInterfaceTraits) GetTimezone() string { + if x != nil { + return x.Timezone + } + return "" +} + +func (x *ApiUserInterfaceTraits) GetDefaultPassword() string { if x != nil { return x.DefaultPassword } return "" } -func (x *ApiGrrUserInterfaceTraits) GetDefaultDownloadsLock() bool { +func (x *ApiUserInterfaceTraits) GetDefaultDownloadsLock() bool { if x != nil { return x.DefaultDownloadsLock } return false } -func (x *ApiGrrUserInterfaceTraits) GetUiSettings() string { +func (x *ApiUserInterfaceTraits) GetUiSettings() string { if x != nil { return x.UiSettings } return "" } -type ApiGrrUser struct { +func (x *ApiUserInterfaceTraits) GetOrg() string { + if x != nil { + return x.Org + } + return "" +} + +func (x *ApiUserInterfaceTraits) GetOrgName() string { + if x != nil { + return x.OrgName + } + return "" +} + +func (x *ApiUserInterfaceTraits) GetBasePath() string { + if x != nil { + return x.BasePath + } + return "" +} + +func (x *ApiUserInterfaceTraits) GetDisableServerEvents() bool { + if x != nil { + return x.DisableServerEvents + } + return false +} + +func (x *ApiUserInterfaceTraits) GetAuthRedirectTemplate() string { + if x != nil { + return x.AuthRedirectTemplate + } + return "" +} + +func (x *ApiUserInterfaceTraits) GetDisableQuarantineButton() bool { + if x != nil { + return x.DisableQuarantineButton + } + return false +} + +// Describe the user to the GUI. +type ApiUser struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` - Settings *GUISettings `protobuf:"bytes,2,opt,name=settings,proto3" json:"settings,omitempty"` - InterfaceTraits *ApiGrrUserInterfaceTraits `protobuf:"bytes,3,opt,name=interface_traits,json=interfaceTraits,proto3" json:"interface_traits,omitempty"` - UserType ApiGrrUser_UserType `protobuf:"varint,4,opt,name=user_type,json=userType,proto3,enum=proto.ApiGrrUser_UserType" json:"user_type,omitempty"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + // Gets constructed from the users.GetUserOptions() + InterfaceTraits *ApiUserInterfaceTraits `protobuf:"bytes,3,opt,name=interface_traits,json=interfaceTraits,proto3" json:"interface_traits,omitempty"` + UserType ApiUser_UserType `protobuf:"varint,4,opt,name=user_type,json=userType,proto3,enum=proto.ApiUser_UserType" json:"user_type,omitempty"` + Orgs []*OrgRecord `protobuf:"bytes,11,rep,name=orgs,proto3" json:"orgs,omitempty"` + OrgAdmin bool `protobuf:"varint,12,opt,name=org_admin,json=orgAdmin,proto3" json:"org_admin,omitempty"` + // Messages for the user are available. + Messages int64 `protobuf:"varint,14,opt,name=messages,proto3" json:"messages,omitempty"` } -func (x *ApiGrrUser) Reset() { - *x = ApiGrrUser{} +func (x *ApiUser) Reset() { + *x = ApiUser{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[4] + mi := &file_users_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *ApiGrrUser) String() string { +func (x *ApiUser) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ApiGrrUser) ProtoMessage() {} +func (*ApiUser) ProtoMessage() {} -func (x *ApiGrrUser) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[4] +func (x *ApiUser) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -630,64 +731,84 @@ func (x *ApiGrrUser) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ApiGrrUser.ProtoReflect.Descriptor instead. -func (*ApiGrrUser) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{4} +// Deprecated: Use ApiUser.ProtoReflect.Descriptor instead. +func (*ApiUser) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{7} } -func (x *ApiGrrUser) GetUsername() string { +func (x *ApiUser) GetUsername() string { if x != nil { return x.Username } return "" } -func (x *ApiGrrUser) GetSettings() *GUISettings { +func (x *ApiUser) GetInterfaceTraits() *ApiUserInterfaceTraits { if x != nil { - return x.Settings + return x.InterfaceTraits } return nil } -func (x *ApiGrrUser) GetInterfaceTraits() *ApiGrrUserInterfaceTraits { +func (x *ApiUser) GetUserType() ApiUser_UserType { if x != nil { - return x.InterfaceTraits + return x.UserType + } + return ApiUser_USER_TYPE_NONE +} + +func (x *ApiUser) GetOrgs() []*OrgRecord { + if x != nil { + return x.Orgs } return nil } -func (x *ApiGrrUser) GetUserType() ApiGrrUser_UserType { +func (x *ApiUser) GetOrgAdmin() bool { if x != nil { - return x.UserType + return x.OrgAdmin + } + return false +} + +func (x *ApiUser) GetMessages() int64 { + if x != nil { + return x.Messages } - return ApiGrrUser_USER_TYPE_NONE + return 0 } -type UserNotificationCount struct { +// Contol the GUI per user. +type GUICustomizations struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Count uint64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` + DisableServerEvents bool `protobuf:"varint,1,opt,name=disable_server_events,json=disableServerEvents,proto3" json:"disable_server_events,omitempty"` + DisableUserManagement bool `protobuf:"varint,2,opt,name=disable_user_management,json=disableUserManagement,proto3" json:"disable_user_management,omitempty"` + DisableQuarantineButton bool `protobuf:"varint,3,opt,name=disable_quarantine_button,json=disableQuarantineButton,proto3" json:"disable_quarantine_button,omitempty"` + // Updated from config_obj.Defaults.HuntExpiryHours + HuntExpiryHours int64 `protobuf:"varint,4,opt,name=hunt_expiry_hours,json=huntExpiryHours,proto3" json:"hunt_expiry_hours,omitempty"` + IndexedClientMetadata []string `protobuf:"bytes,5,rep,name=indexed_client_metadata,json=indexedClientMetadata,proto3" json:"indexed_client_metadata,omitempty"` } -func (x *UserNotificationCount) Reset() { - *x = UserNotificationCount{} +func (x *GUICustomizations) Reset() { + *x = GUICustomizations{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[5] + mi := &file_users_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *UserNotificationCount) String() string { +func (x *GUICustomizations) String() string { return protoimpl.X.MessageStringOf(x) } -func (*UserNotificationCount) ProtoMessage() {} +func (*GUICustomizations) ProtoMessage() {} -func (x *UserNotificationCount) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[5] +func (x *GUICustomizations) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -698,47 +819,91 @@ func (x *UserNotificationCount) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use UserNotificationCount.ProtoReflect.Descriptor instead. -func (*UserNotificationCount) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{5} +// Deprecated: Use GUICustomizations.ProtoReflect.Descriptor instead. +func (*GUICustomizations) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{8} } -func (x *UserNotificationCount) GetCount() uint64 { +func (x *GUICustomizations) GetDisableServerEvents() bool { if x != nil { - return x.Count + return x.DisableServerEvents + } + return false +} + +func (x *GUICustomizations) GetDisableUserManagement() bool { + if x != nil { + return x.DisableUserManagement + } + return false +} + +func (x *GUICustomizations) GetDisableQuarantineButton() bool { + if x != nil { + return x.DisableQuarantineButton + } + return false +} + +func (x *GUICustomizations) GetHuntExpiryHours() int64 { + if x != nil { + return x.HuntExpiryHours } return 0 } -type UserNotification struct { +func (x *GUICustomizations) GetIndexedClientMetadata() []string { + if x != nil { + return x.IndexedClientMetadata + } + return nil +} + +type SetGUIOptionsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` - NotificationType UserNotification_Type `protobuf:"varint,2,opt,name=notification_type,json=notificationType,proto3,enum=proto.UserNotification_Type" json:"notification_type,omitempty"` - State UserNotification_State `protobuf:"varint,3,opt,name=state,proto3,enum=proto.UserNotification_State" json:"state,omitempty"` - Timestamp uint64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + Theme string `protobuf:"bytes,2,opt,name=theme,proto3" json:"theme,omitempty"` + Timezone string `protobuf:"bytes,6,opt,name=timezone,proto3" json:"timezone,omitempty"` + Lang string `protobuf:"bytes,5,opt,name=lang,proto3" json:"lang,omitempty"` + // Downloads will be protected using this password. If this is + // empty we do not update the password. If it is set to "-" we + // reset the password to the empty string. + DefaultPassword string `protobuf:"bytes,3,opt,name=default_password,json=defaultPassword,proto3" json:"default_password,omitempty"` + // Offer to protect download exports by default. + DefaultDownloadsLock bool `protobuf:"varint,4,opt,name=default_downloads_lock,json=defaultDownloadsLock,proto3" json:"default_downloads_lock,omitempty"` + Options string `protobuf:"bytes,1,opt,name=options,proto3" json:"options,omitempty"` + // Current org id + Org string `protobuf:"bytes,7,opt,name=org,proto3" json:"org,omitempty"` + Customizations *GUICustomizations `protobuf:"bytes,8,opt,name=customizations,proto3" json:"customizations,omitempty"` + Links []*proto1.GUILink `protobuf:"bytes,9,rep,name=links,proto3" json:"links,omitempty"` + // Optional features of the UI to disable: + // TODO: Move to GUICustomizations. + DisableServerEvents bool `protobuf:"varint,26,opt,name=disable_server_events,json=disableServerEvents,proto3" json:"disable_server_events,omitempty"` + AuthRedirectTemplate string `protobuf:"bytes,27,opt,name=auth_redirect_template,json=authRedirectTemplate,proto3" json:"auth_redirect_template,omitempty"` + DisableQuarantineButton bool `protobuf:"varint,28,opt,name=disable_quarantine_button,json=disableQuarantineButton,proto3" json:"disable_quarantine_button,omitempty"` + // How many notifications are currently outstanding for the user. + Messages int64 `protobuf:"varint,29,opt,name=messages,proto3" json:"messages,omitempty"` } -func (x *UserNotification) Reset() { - *x = UserNotification{} +func (x *SetGUIOptionsRequest) Reset() { + *x = SetGUIOptionsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[6] + mi := &file_users_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *UserNotification) String() string { +func (x *SetGUIOptionsRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*UserNotification) ProtoMessage() {} +func (*SetGUIOptionsRequest) ProtoMessage() {} -func (x *UserNotification) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[6] +func (x *SetGUIOptionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -749,71 +914,131 @@ func (x *UserNotification) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use UserNotification.ProtoReflect.Descriptor instead. -func (*UserNotification) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{6} +// Deprecated: Use SetGUIOptionsRequest.ProtoReflect.Descriptor instead. +func (*SetGUIOptionsRequest) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{9} } -func (x *UserNotification) GetUsername() string { +func (x *SetGUIOptionsRequest) GetTheme() string { if x != nil { - return x.Username + return x.Theme } return "" } -func (x *UserNotification) GetNotificationType() UserNotification_Type { +func (x *SetGUIOptionsRequest) GetTimezone() string { if x != nil { - return x.NotificationType + return x.Timezone } - return UserNotification_TYPE_UNSET + return "" } -func (x *UserNotification) GetState() UserNotification_State { +func (x *SetGUIOptionsRequest) GetLang() string { if x != nil { - return x.State + return x.Lang } - return UserNotification_STATE_UNSET + return "" } -func (x *UserNotification) GetTimestamp() uint64 { +func (x *SetGUIOptionsRequest) GetDefaultPassword() string { if x != nil { - return x.Timestamp + return x.DefaultPassword } - return 0 + return "" } -func (x *UserNotification) GetMessage() string { +func (x *SetGUIOptionsRequest) GetDefaultDownloadsLock() bool { if x != nil { - return x.Message + return x.DefaultDownloadsLock + } + return false +} + +func (x *SetGUIOptionsRequest) GetOptions() string { + if x != nil { + return x.Options } return "" } -type GetUserNotificationsResponse struct { +func (x *SetGUIOptionsRequest) GetOrg() string { + if x != nil { + return x.Org + } + return "" +} + +func (x *SetGUIOptionsRequest) GetCustomizations() *GUICustomizations { + if x != nil { + return x.Customizations + } + return nil +} + +func (x *SetGUIOptionsRequest) GetLinks() []*proto1.GUILink { + if x != nil { + return x.Links + } + return nil +} + +func (x *SetGUIOptionsRequest) GetDisableServerEvents() bool { + if x != nil { + return x.DisableServerEvents + } + return false +} + +func (x *SetGUIOptionsRequest) GetAuthRedirectTemplate() string { + if x != nil { + return x.AuthRedirectTemplate + } + return "" +} + +func (x *SetGUIOptionsRequest) GetDisableQuarantineButton() bool { + if x != nil { + return x.DisableQuarantineButton + } + return false +} + +func (x *SetGUIOptionsRequest) GetMessages() int64 { + if x != nil { + return x.Messages + } + return 0 +} + +type SetGUIOptionsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Items []*UserNotification `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + // If this is specifed the GUI will redirect to the specified + // URL. This helps when there are multiple servers that can handle + // the user and a better one is needed. Normally this is really + // set by any middleware (e.g. custom authenticators). + RedirectUrl string `protobuf:"bytes,1,opt,name=redirect_url,json=redirectUrl,proto3" json:"redirect_url,omitempty"` } -func (x *GetUserNotificationsResponse) Reset() { - *x = GetUserNotificationsResponse{} +func (x *SetGUIOptionsResponse) Reset() { + *x = SetGUIOptionsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[7] + mi := &file_users_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *GetUserNotificationsResponse) String() string { +func (x *SetGUIOptionsResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetUserNotificationsResponse) ProtoMessage() {} +func (*SetGUIOptionsResponse) ProtoMessage() {} -func (x *GetUserNotificationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[7] +func (x *SetGUIOptionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -824,43 +1049,43 @@ func (x *GetUserNotificationsResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GetUserNotificationsResponse.ProtoReflect.Descriptor instead. -func (*GetUserNotificationsResponse) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{7} +// Deprecated: Use SetGUIOptionsResponse.ProtoReflect.Descriptor instead. +func (*SetGUIOptionsResponse) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{10} } -func (x *GetUserNotificationsResponse) GetItems() []*UserNotification { +func (x *SetGUIOptionsResponse) GetRedirectUrl() string { if x != nil { - return x.Items + return x.RedirectUrl } - return nil + return "" } -type GetUserNotificationsRequest struct { +type Users struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ClearPending bool `protobuf:"varint,1,opt,name=clear_pending,json=clearPending,proto3" json:"clear_pending,omitempty"` + Users []*VelociraptorUser `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` } -func (x *GetUserNotificationsRequest) Reset() { - *x = GetUserNotificationsRequest{} +func (x *Users) Reset() { + *x = Users{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[8] + mi := &file_users_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *GetUserNotificationsRequest) String() string { +func (x *Users) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetUserNotificationsRequest) ProtoMessage() {} +func (*Users) ProtoMessage() {} -func (x *GetUserNotificationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[8] +func (x *Users) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -871,48 +1096,52 @@ func (x *GetUserNotificationsRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GetUserNotificationsRequest.ProtoReflect.Descriptor instead. -func (*GetUserNotificationsRequest) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{8} +// Deprecated: Use Users.ProtoReflect.Descriptor instead. +func (*Users) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{11} } -func (x *GetUserNotificationsRequest) GetClearPending() bool { +func (x *Users) GetUsers() []*VelociraptorUser { if x != nil { - return x.ClearPending + return x.Users } - return false + return nil } -type SetGUIOptionsRequest struct { +// Get the roles and permissions of a user within the org. +type UserRoles struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Theme string `protobuf:"bytes,2,opt,name=theme,proto3" json:"theme,omitempty"` - // Downloads will be protected using this password. - DefaultPassword string `protobuf:"bytes,3,opt,name=default_password,json=defaultPassword,proto3" json:"default_password,omitempty"` - // Offer to protect download exports by default. - DefaultDownloadsLock bool `protobuf:"varint,4,opt,name=default_downloads_lock,json=defaultDownloadsLock,proto3" json:"default_downloads_lock,omitempty"` - Options string `protobuf:"bytes,1,opt,name=options,proto3" json:"options,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Org string `protobuf:"bytes,2,opt,name=org,proto3" json:"org,omitempty"` + OrgName string `protobuf:"bytes,8,opt,name=org_name,json=orgName,proto3" json:"org_name,omitempty"` + Permissions []string `protobuf:"bytes,3,rep,name=permissions,proto3" json:"permissions,omitempty"` + Roles []string `protobuf:"bytes,4,rep,name=roles,proto3" json:"roles,omitempty"` + // Expanded permissions as above. + EffectivePermissions []string `protobuf:"bytes,5,rep,name=effective_permissions,json=effectivePermissions,proto3" json:"effective_permissions,omitempty"` + AllRoles []string `protobuf:"bytes,6,rep,name=all_roles,json=allRoles,proto3" json:"all_roles,omitempty"` + AllPermissions []string `protobuf:"bytes,7,rep,name=all_permissions,json=allPermissions,proto3" json:"all_permissions,omitempty"` } -func (x *SetGUIOptionsRequest) Reset() { - *x = SetGUIOptionsRequest{} +func (x *UserRoles) Reset() { + *x = UserRoles{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[9] + mi := &file_users_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *SetGUIOptionsRequest) String() string { +func (x *UserRoles) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SetGUIOptionsRequest) ProtoMessage() {} +func (*UserRoles) ProtoMessage() {} -func (x *SetGUIOptionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[9] +func (x *UserRoles) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -923,64 +1152,93 @@ func (x *SetGUIOptionsRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SetGUIOptionsRequest.ProtoReflect.Descriptor instead. -func (*SetGUIOptionsRequest) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{9} +// Deprecated: Use UserRoles.ProtoReflect.Descriptor instead. +func (*UserRoles) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{12} } -func (x *SetGUIOptionsRequest) GetTheme() string { +func (x *UserRoles) GetName() string { if x != nil { - return x.Theme + return x.Name } return "" } -func (x *SetGUIOptionsRequest) GetDefaultPassword() string { +func (x *UserRoles) GetOrg() string { if x != nil { - return x.DefaultPassword + return x.Org } return "" } -func (x *SetGUIOptionsRequest) GetDefaultDownloadsLock() bool { +func (x *UserRoles) GetOrgName() string { if x != nil { - return x.DefaultDownloadsLock + return x.OrgName } - return false + return "" } -func (x *SetGUIOptionsRequest) GetOptions() string { +func (x *UserRoles) GetPermissions() []string { if x != nil { - return x.Options + return x.Permissions } - return "" + return nil } -type Users struct { +func (x *UserRoles) GetRoles() []string { + if x != nil { + return x.Roles + } + return nil +} + +func (x *UserRoles) GetEffectivePermissions() []string { + if x != nil { + return x.EffectivePermissions + } + return nil +} + +func (x *UserRoles) GetAllRoles() []string { + if x != nil { + return x.AllRoles + } + return nil +} + +func (x *UserRoles) GetAllPermissions() []string { + if x != nil { + return x.AllPermissions + } + return nil +} + +type SetPasswordRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Users []*VelociraptorUser `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` } -func (x *Users) Reset() { - *x = Users{} +func (x *SetPasswordRequest) Reset() { + *x = SetPasswordRequest{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[10] + mi := &file_users_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Users) String() string { +func (x *SetPasswordRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Users) ProtoMessage() {} +func (*SetPasswordRequest) ProtoMessage() {} -func (x *Users) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[10] +func (x *SetPasswordRequest) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -991,16 +1249,23 @@ func (x *Users) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Users.ProtoReflect.Descriptor instead. -func (*Users) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{10} +// Deprecated: Use SetPasswordRequest.ProtoReflect.Descriptor instead. +func (*SetPasswordRequest) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{13} } -func (x *Users) GetUsers() []*VelociraptorUser { +func (x *SetPasswordRequest) GetPassword() string { if x != nil { - return x.Users + return x.Password } - return nil + return "" +} + +func (x *SetPasswordRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" } // Store favorite collections (essential preset collection specs) @@ -1011,14 +1276,14 @@ type Favorite struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - Spec []*proto1.ArtifactSpec `protobuf:"bytes,3,rep,name=spec,proto3" json:"spec,omitempty"` + Spec []*proto2.ArtifactSpec `protobuf:"bytes,3,rep,name=spec,proto3" json:"spec,omitempty"` Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` } func (x *Favorite) Reset() { *x = Favorite{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[11] + mi := &file_users_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1031,7 +1296,7 @@ func (x *Favorite) String() string { func (*Favorite) ProtoMessage() {} func (x *Favorite) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[11] + mi := &file_users_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1044,7 +1309,7 @@ func (x *Favorite) ProtoReflect() protoreflect.Message { // Deprecated: Use Favorite.ProtoReflect.Descriptor instead. func (*Favorite) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{11} + return file_users_proto_rawDescGZIP(), []int{14} } func (x *Favorite) GetName() string { @@ -1061,7 +1326,7 @@ func (x *Favorite) GetDescription() string { return "" } -func (x *Favorite) GetSpec() []*proto1.ArtifactSpec { +func (x *Favorite) GetSpec() []*proto2.ArtifactSpec { if x != nil { return x.Spec } @@ -1086,7 +1351,7 @@ type Favorites struct { func (x *Favorites) Reset() { *x = Favorites{} if protoimpl.UnsafeEnabled { - mi := &file_users_proto_msgTypes[12] + mi := &file_users_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1099,7 +1364,7 @@ func (x *Favorites) String() string { func (*Favorites) ProtoMessage() {} func (x *Favorites) ProtoReflect() protoreflect.Message { - mi := &file_users_proto_msgTypes[12] + mi := &file_users_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1112,7 +1377,7 @@ func (x *Favorites) ProtoReflect() protoreflect.Message { // Deprecated: Use Favorites.ProtoReflect.Descriptor instead. func (*Favorites) Descriptor() ([]byte, []int) { - return file_users_proto_rawDescGZIP(), []int{12} + return file_users_proto_rawDescGZIP(), []int{15} } func (x *Favorites) GetItems() []*Favorite { @@ -1129,209 +1394,251 @@ var file_users_proto_rawDesc = []byte{ 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x61, 0x63, 0x6c, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x63, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x24, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf9, 0x03, 0x0a, 0x10, 0x56, 0x65, 0x6c, 0x6f, 0x63, - 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x14, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, - 0x0e, 0x12, 0x0c, 0x54, 0x68, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x49, 0x0a, 0x0d, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x24, 0xe2, 0xfc, - 0xe3, 0xc4, 0x01, 0x1e, 0x12, 0x1c, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x20, 0x68, 0x61, 0x73, - 0x68, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x2e, 0x52, 0x0c, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x61, 0x73, 0x68, - 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x73, 0x61, 0x6c, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x53, 0x61, 0x6c, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x70, - 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x69, - 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, - 0x64, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x76, - 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x72, 0x0a, 0x09, - 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x42, - 0x55, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x4f, 0x12, 0x4d, 0x41, 0x20, 0x72, 0x65, 0x61, 0x64, 0x20, - 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x75, 0x73, - 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x47, 0x55, 0x49, 0x20, 0x62, 0x75, 0x74, 0x20, 0x69, 0x73, - 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, - 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x20, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x20, 0x6f, 0x72, 0x20, - 0x68, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, - 0x12, 0x49, 0x0a, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, - 0x42, 0x31, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x2b, 0x12, 0x29, 0x49, 0x66, 0x20, 0x73, 0x65, 0x74, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x6e, 0x6f, 0x74, - 0x20, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x67, 0x20, - 0x69, 0x6e, 0x2e, 0x52, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x35, 0x0a, 0x0b, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x41, 0x43, 0x4c, 0x52, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x22, 0x8e, 0x02, 0x0a, 0x0b, 0x47, 0x55, 0x49, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, - 0x67, 0x73, 0x12, 0x4b, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x55, 0x49, 0x53, 0x65, 0x74, 0x74, - 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x55, 0x49, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x1c, 0xe2, 0xfc, 0xe3, - 0xc4, 0x01, 0x16, 0x12, 0x14, 0x55, 0x73, 0x65, 0x72, 0x20, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, - 0x61, 0x63, 0x65, 0x20, 0x6d, 0x6f, 0x64, 0x65, 0x2e, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, - 0x58, 0x0a, 0x0b, 0x63, 0x61, 0x6e, 0x61, 0x72, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x42, 0x37, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x31, 0x12, 0x2f, 0x49, 0x66, - 0x20, 0x74, 0x72, 0x75, 0x65, 0x2c, 0x20, 0x73, 0x68, 0x6f, 0x77, 0x20, 0x66, 0x65, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x61, 0x72, 0x65, 0x20, 0x62, 0x65, - 0x69, 0x6e, 0x67, 0x20, 0x63, 0x61, 0x6e, 0x61, 0x72, 0x69, 0x65, 0x64, 0x2e, 0x52, 0x0a, 0x63, - 0x61, 0x6e, 0x61, 0x72, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0x2c, 0x0a, 0x06, 0x55, 0x49, 0x4d, - 0x6f, 0x64, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x42, 0x41, 0x53, 0x49, 0x43, 0x10, 0x00, 0x12, 0x0c, - 0x0a, 0x08, 0x41, 0x44, 0x56, 0x41, 0x4e, 0x43, 0x45, 0x44, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, - 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x02, 0x3a, 0x2a, 0xda, 0xfc, 0xe3, 0xc4, 0x01, 0x24, 0x0a, - 0x22, 0x55, 0x73, 0x65, 0x72, 0x20, 0x47, 0x55, 0x49, 0x20, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, - 0x67, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, - 0x65, 0x73, 0x2e, 0x22, 0x2e, 0x0a, 0x06, 0x55, 0x49, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, - 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, - 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x75, 0x72, 0x6c, 0x22, 0xd5, 0x02, 0x0a, 0x19, 0x41, 0x70, 0x69, 0x47, 0x72, 0x72, 0x55, 0x73, - 0x65, 0x72, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x69, 0x74, - 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, - 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x43, 0x4c, 0x52, 0x0b, 0x50, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x61, 0x75, 0x74, 0x68, - 0x5f, 0x75, 0x73, 0x69, 0x6e, 0x67, 0x5f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x18, 0x13, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x55, 0x73, 0x69, 0x6e, 0x67, 0x47, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, - 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x23, - 0x0a, 0x05, 0x6c, 0x69, 0x6e, 0x6b, 0x73, 0x18, 0x15, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x49, 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x05, 0x6c, 0x69, - 0x6e, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x66, - 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x12, 0x34, 0x0a, 0x16, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, - 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x44, 0x6f, 0x77, - 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x4c, 0x6f, 0x63, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x69, - 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x75, 0x69, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xaf, 0x03, 0x0a, 0x0a, - 0x41, 0x70, 0x69, 0x47, 0x72, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x08, 0x75, 0x73, + 0x1a, 0x19, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x66, 0x6c, 0x6f, + 0x77, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x0a, 0x6f, 0x72, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x23, 0x0a, + 0x07, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x74, 0x72, 0x69, 0x6e, + 0x67, 0x73, 0x22, 0x5d, 0x0a, 0x09, 0x55, 0x73, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, + 0x28, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x41, + 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x6c, 0x61, 0x73, + 0x74, 0x5f, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x22, 0xe8, 0x04, 0x0a, 0x10, 0x56, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, + 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x14, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x0e, 0x12, 0x0c, 0x54, 0x68, + 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x49, 0x0a, 0x0d, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x61, 0x73, + 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x24, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x1e, 0x12, + 0x1c, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x20, 0x68, 0x61, 0x73, 0x68, 0x20, 0x6f, 0x66, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x2e, 0x52, 0x0c, 0x70, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x61, 0x73, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x70, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0c, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x53, 0x61, 0x6c, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, + 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, + 0x65, 0x64, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x72, 0x0a, 0x09, 0x72, 0x65, 0x61, 0x64, 0x5f, + 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x42, 0x55, 0xe2, 0xfc, 0xe3, 0xc4, + 0x01, 0x4f, 0x12, 0x4d, 0x41, 0x20, 0x72, 0x65, 0x61, 0x64, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, + 0x75, 0x73, 0x65, 0x72, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x75, 0x73, 0x65, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x47, 0x55, 0x49, 0x20, 0x62, 0x75, 0x74, 0x20, 0x69, 0x73, 0x20, 0x6e, 0x6f, 0x74, 0x20, + 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x61, 0x75, 0x6e, 0x63, + 0x68, 0x20, 0x66, 0x6c, 0x6f, 0x77, 0x73, 0x20, 0x6f, 0x72, 0x20, 0x68, 0x75, 0x6e, 0x74, 0x73, + 0x2e, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x49, 0x0a, 0x06, 0x6c, + 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x42, 0x31, 0xe2, 0xfc, 0xe3, + 0xc4, 0x01, 0x2b, 0x12, 0x29, 0x49, 0x66, 0x20, 0x73, 0x65, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x75, 0x73, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x6f, 0x67, 0x20, 0x69, 0x6e, 0x2e, 0x52, 0x06, + 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x35, 0x0a, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x43, 0x4c, + 0x52, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x24, 0x0a, + 0x04, 0x6f, 0x72, 0x67, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x4f, 0x72, 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x04, 0x6f, + 0x72, 0x67, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x6f, + 0x72, 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, + 0x74, 0x4f, 0x72, 0x67, 0x12, 0x26, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x0d, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, + 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0xc5, 0x01, 0x0a, + 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x28, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x14, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x0e, 0x12, 0x0c, 0x54, 0x68, 0x65, 0x20, 0x75, 0x73, + 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3a, 0x0a, 0x08, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, + 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x18, 0x12, 0x16, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x65, 0x61, + 0x72, 0x74, 0x65, 0x78, 0x74, 0x20, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x08, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6f, 0x72, 0x67, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6f, 0x72, 0x67, 0x73, 0x12, 0x14, 0x0a, 0x05, + 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x72, 0x6f, 0x6c, + 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x61, 0x64, 0x64, 0x5f, 0x6e, 0x65, 0x77, 0x5f, 0x75, 0x73, + 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x64, 0x64, 0x4e, 0x65, 0x77, + 0x55, 0x73, 0x65, 0x72, 0x22, 0x91, 0x01, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x14, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x0e, + 0x12, 0x0c, 0x54, 0x68, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x52, 0x0a, 0x04, 0x6f, 0x72, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x09, 0x42, 0x3e, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x38, 0x12, 0x36, 0x54, 0x68, 0x65, 0x20, + 0x6f, 0x72, 0x67, 0x20, 0x49, 0x44, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x28, 0x65, 0x6d, 0x70, + 0x74, 0x79, 0x20, 0x6d, 0x65, 0x61, 0x6e, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x6f, 0x72, 0x67, + 0x73, 0x29, 0x52, 0x04, 0x6f, 0x72, 0x67, 0x73, 0x22, 0x33, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6f, + 0x72, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6f, 0x72, 0x67, 0x22, 0xae, 0x05, + 0x0a, 0x16, 0x41, 0x70, 0x69, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, + 0x63, 0x65, 0x54, 0x72, 0x61, 0x69, 0x74, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x50, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, + 0x43, 0x4c, 0x52, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x40, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x47, 0x55, 0x49, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x18, 0x16, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x5f, 0x6c, 0x65, 0x73, 0x73, 0x18, 0x19, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x70, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x65, 0x73, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x69, + 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x69, 0x63, + 0x74, 0x75, 0x72, 0x65, 0x12, 0x24, 0x0a, 0x05, 0x6c, 0x69, 0x6e, 0x6b, 0x73, 0x18, 0x15, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x55, 0x49, 0x4c, + 0x69, 0x6e, 0x6b, 0x52, 0x05, 0x6c, 0x69, 0x6e, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x68, + 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x17, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x12, 0x29, 0x0a, 0x10, + 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x34, 0x0a, 0x16, 0x64, 0x65, 0x66, 0x61, 0x75, + 0x6c, 0x74, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x5f, 0x6c, 0x6f, 0x63, + 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, + 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x4c, 0x6f, 0x63, 0x6b, 0x12, 0x1f, 0x0a, + 0x0b, 0x75, 0x69, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x75, 0x69, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x10, + 0x0a, 0x03, 0x6f, 0x72, 0x67, 0x18, 0x18, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6f, 0x72, 0x67, + 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x72, 0x67, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6f, 0x72, 0x67, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x62, + 0x61, 0x73, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x62, 0x61, 0x73, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x32, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x16, + 0x61, 0x75, 0x74, 0x68, 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x65, + 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x61, 0x75, + 0x74, 0x68, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, + 0x74, 0x65, 0x12, 0x3a, 0x0a, 0x19, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x71, 0x75, + 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, 0x5f, 0x62, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x18, + 0x1c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x51, 0x75, + 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, 0x42, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x22, 0xe4, + 0x03, 0x0a, 0x07, 0x41, 0x70, 0x69, 0x55, 0x73, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1d, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x17, 0x12, 0x15, 0x54, 0x68, 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x52, 0x08, 0x75, 0x73, 0x65, - 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x49, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, - 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x47, 0x55, 0x49, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x42, 0x19, 0xe2, 0xfc, 0xe3, - 0xc4, 0x01, 0x13, 0x12, 0x11, 0x55, 0x73, 0x65, 0x72, 0x20, 0x55, 0x49, 0x20, 0x73, 0x65, 0x74, - 0x74, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, - 0x12, 0x94, 0x01, 0x0a, 0x10, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x5f, 0x74, - 0x72, 0x61, 0x69, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x47, 0x72, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x69, 0x74, 0x73, 0x42, 0x47, 0xe2, - 0xfc, 0xe3, 0xc4, 0x01, 0x41, 0x12, 0x3f, 0x55, 0x73, 0x65, 0x72, 0x27, 0x73, 0x20, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x20, 0x74, 0x72, 0x61, 0x69, 0x74, 0x73, 0x20, 0x28, - 0x77, 0x68, 0x61, 0x74, 0x20, 0x74, 0x68, 0x65, 0x79, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x61, 0x6e, - 0x64, 0x20, 0x63, 0x61, 0x6e, 0x27, 0x74, 0x20, 0x64, 0x6f, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x55, 0x49, 0x29, 0x2e, 0x52, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, - 0x65, 0x54, 0x72, 0x61, 0x69, 0x74, 0x73, 0x12, 0x37, 0x0a, 0x09, 0x75, 0x73, 0x65, 0x72, 0x5f, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x47, 0x72, 0x72, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x55, 0x73, - 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, - 0x22, 0x4b, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x0e, - 0x55, 0x53, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, - 0x12, 0x16, 0x0a, 0x12, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, - 0x41, 0x4e, 0x44, 0x41, 0x52, 0x44, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x55, 0x53, 0x45, 0x52, - 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x44, 0x4d, 0x49, 0x4e, 0x10, 0x02, 0x22, 0x2d, 0x0a, - 0x15, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xfa, 0x06, 0x0a, - 0x10, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x49, 0x0a, - 0x11, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, - 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x10, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x33, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x31, 0x0a, - 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, - 0x42, 0x13, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x0d, 0x0a, 0x0b, 0x52, 0x44, 0x46, 0x44, 0x61, 0x74, - 0x65, 0x74, 0x69, 0x6d, 0x65, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xb8, 0x04, 0x0a, 0x04, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x45, - 0x54, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4c, 0x49, 0x45, - 0x4e, 0x54, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x52, 0x4f, 0x47, 0x41, 0x54, 0x45, 0x44, 0x10, - 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, - 0x5f, 0x41, 0x50, 0x50, 0x52, 0x4f, 0x56, 0x41, 0x4c, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, - 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x20, 0x0a, 0x1c, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x48, 0x55, - 0x4e, 0x54, 0x5f, 0x41, 0x50, 0x50, 0x52, 0x4f, 0x56, 0x41, 0x4c, 0x5f, 0x52, 0x45, 0x51, 0x55, - 0x45, 0x53, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x24, 0x0a, 0x20, 0x54, 0x59, 0x50, 0x45, 0x5f, - 0x43, 0x52, 0x4f, 0x4e, 0x5f, 0x4a, 0x4f, 0x42, 0x5f, 0x41, 0x50, 0x50, 0x52, 0x4f, 0x56, 0x41, - 0x4c, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x45, 0x44, 0x10, 0x04, 0x12, 0x20, 0x0a, - 0x1c, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x41, 0x50, 0x50, - 0x52, 0x4f, 0x56, 0x41, 0x4c, 0x5f, 0x47, 0x52, 0x41, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x05, 0x12, - 0x1e, 0x0a, 0x1a, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x48, 0x55, 0x4e, 0x54, 0x5f, 0x41, 0x50, 0x50, - 0x52, 0x4f, 0x56, 0x41, 0x4c, 0x5f, 0x47, 0x52, 0x41, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x06, 0x12, - 0x22, 0x0a, 0x1e, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x4f, 0x4e, 0x5f, 0x4a, 0x4f, 0x42, - 0x5f, 0x41, 0x50, 0x50, 0x52, 0x4f, 0x56, 0x41, 0x4c, 0x5f, 0x47, 0x52, 0x41, 0x4e, 0x54, 0x45, - 0x44, 0x10, 0x07, 0x12, 0x1b, 0x0a, 0x17, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x56, 0x46, 0x53, 0x5f, - 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x08, - 0x12, 0x23, 0x0a, 0x1f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x56, 0x46, 0x53, 0x5f, 0x46, 0x49, 0x4c, - 0x45, 0x5f, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x46, 0x41, 0x49, - 0x4c, 0x45, 0x44, 0x10, 0x09, 0x12, 0x15, 0x0a, 0x11, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x48, 0x55, - 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x0a, 0x12, 0x1f, 0x0a, 0x1b, - 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x41, 0x52, 0x43, 0x48, 0x49, 0x56, - 0x45, 0x5f, 0x47, 0x45, 0x4e, 0x45, 0x52, 0x41, 0x54, 0x45, 0x44, 0x10, 0x0b, 0x12, 0x27, 0x0a, - 0x23, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x41, 0x52, 0x43, 0x48, 0x49, - 0x56, 0x45, 0x5f, 0x47, 0x45, 0x4e, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x46, 0x41, - 0x49, 0x4c, 0x45, 0x44, 0x10, 0x0c, 0x12, 0x1b, 0x0a, 0x17, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, - 0x4c, 0x4f, 0x57, 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, - 0x44, 0x10, 0x0d, 0x12, 0x18, 0x0a, 0x14, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, 0x4c, 0x4f, 0x57, - 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x0e, 0x12, 0x25, 0x0a, - 0x21, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x56, 0x46, 0x53, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x5f, 0x44, - 0x49, 0x52, 0x45, 0x43, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, - 0x45, 0x44, 0x10, 0x0f, 0x12, 0x2f, 0x0a, 0x2b, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x56, 0x46, 0x53, - 0x5f, 0x52, 0x45, 0x43, 0x55, 0x52, 0x53, 0x49, 0x56, 0x45, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x5f, - 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, - 0x54, 0x45, 0x44, 0x10, 0x10, 0x22, 0x42, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0f, - 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x45, 0x54, 0x10, 0x00, 0x12, - 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, - 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, - 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x22, 0x4d, 0x0a, 0x1c, 0x47, 0x65, 0x74, - 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x69, 0x74, 0x65, - 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x71, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x55, - 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x52, 0x0a, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x72, - 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x2d, - 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x27, 0x12, 0x25, 0x49, 0x66, 0x20, 0x73, 0x65, 0x74, 0x2c, 0x20, - 0x63, 0x6c, 0x65, 0x61, 0x72, 0x73, 0x20, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x6e, - 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, 0x0c, 0x63, - 0x6c, 0x65, 0x61, 0x72, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x22, 0xa7, 0x01, 0x0a, 0x14, - 0x53, 0x65, 0x74, 0x47, 0x55, 0x49, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, - 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x34, 0x0a, 0x16, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, - 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x44, 0x6f, - 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x4c, 0x6f, 0x63, 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x36, 0x0a, 0x05, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x2d, - 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, - 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x7d, 0x0a, - 0x08, 0x46, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, - 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x27, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x53, 0x70, - 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x32, 0x0a, 0x09, - 0x46, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x05, 0x69, 0x74, 0x65, - 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x46, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, - 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, - 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x91, 0x01, 0x0a, 0x10, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, + 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x69, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x55, 0x73, 0x65, 0x72, + 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x69, 0x74, 0x73, 0x42, + 0x47, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x41, 0x12, 0x3f, 0x55, 0x73, 0x65, 0x72, 0x27, 0x73, 0x20, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x20, 0x74, 0x72, 0x61, 0x69, 0x74, 0x73, + 0x20, 0x28, 0x77, 0x68, 0x61, 0x74, 0x20, 0x74, 0x68, 0x65, 0x79, 0x20, 0x63, 0x61, 0x6e, 0x20, + 0x61, 0x6e, 0x64, 0x20, 0x63, 0x61, 0x6e, 0x27, 0x74, 0x20, 0x64, 0x6f, 0x20, 0x69, 0x6e, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x55, 0x49, 0x29, 0x2e, 0x52, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, + 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x69, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x09, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x70, 0x69, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x55, 0x73, 0x65, + 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x24, 0x0a, 0x04, 0x6f, 0x72, 0x67, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4f, 0x72, 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, + 0x04, 0x6f, 0x72, 0x67, 0x73, 0x12, 0x45, 0x0a, 0x09, 0x6f, 0x72, 0x67, 0x5f, 0x61, 0x64, 0x6d, + 0x69, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x42, 0x28, 0xe2, 0xfc, 0xe3, 0xc4, 0x01, 0x22, + 0x12, 0x20, 0x57, 0x68, 0x65, 0x74, 0x68, 0x65, 0x72, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x75, + 0x73, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x61, 0x6e, 0x20, 0x4f, 0x72, 0x67, 0x41, 0x64, 0x6d, + 0x69, 0x6e, 0x52, 0x08, 0x6f, 0x72, 0x67, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x4b, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x0e, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x55, 0x53, 0x45, 0x52, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x4e, 0x44, 0x41, 0x52, 0x44, 0x10, 0x01, + 0x12, 0x13, 0x0a, 0x0f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x44, + 0x4d, 0x49, 0x4e, 0x10, 0x02, 0x22, 0x9f, 0x02, 0x0a, 0x11, 0x47, 0x55, 0x49, 0x43, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x65, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, + 0x36, 0x0a, 0x17, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3a, 0x0a, 0x19, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x5f, 0x71, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, 0x5f, 0x62, 0x75, + 0x74, 0x74, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x51, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, 0x42, 0x75, 0x74, + 0x74, 0x6f, 0x6e, 0x12, 0x2a, 0x0a, 0x11, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x79, 0x5f, 0x68, 0x6f, 0x75, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, + 0x68, 0x75, 0x6e, 0x74, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x12, + 0x36, 0x0a, 0x17, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x64, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x15, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x64, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x93, 0x04, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x47, + 0x55, 0x49, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, + 0x6e, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, + 0x6e, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, + 0x74, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x12, 0x34, 0x0a, 0x16, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x64, 0x6f, 0x77, + 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, + 0x61, 0x64, 0x73, 0x4c, 0x6f, 0x63, 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x10, 0x0a, 0x03, 0x6f, 0x72, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6f, 0x72, 0x67, 0x12, 0x40, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x55, 0x49, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x24, 0x0a, 0x05, 0x6c, 0x69, 0x6e, 0x6b, 0x73, 0x18, 0x09, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x55, 0x49, + 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x05, 0x6c, 0x69, 0x6e, 0x6b, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x65, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, + 0x34, 0x0a, 0x16, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x14, 0x61, 0x75, 0x74, 0x68, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x54, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x3a, 0x0a, 0x19, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x5f, 0x71, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, 0x5f, 0x62, 0x75, 0x74, 0x74, + 0x6f, 0x6e, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, + 0x65, 0x51, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, 0x42, 0x75, 0x74, 0x74, 0x6f, + 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x1d, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x3a, 0x0a, + 0x15, 0x53, 0x65, 0x74, 0x47, 0x55, 0x49, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x72, 0x6c, 0x22, 0x36, 0x0a, 0x05, 0x55, 0x73, 0x65, + 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x65, 0x6c, 0x6f, 0x63, 0x69, + 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, + 0x73, 0x22, 0xff, 0x01, 0x0a, 0x09, 0x55, 0x73, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6f, 0x72, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6f, 0x72, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x72, 0x67, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x72, 0x67, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x15, 0x65, 0x66, 0x66, 0x65, + 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x0a, + 0x09, 0x61, 0x6c, 0x6c, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x08, 0x61, 0x6c, 0x6c, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x6c, + 0x6c, 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x6c, 0x6c, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x22, 0x4c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x7d, 0x0a, 0x08, 0x46, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x12, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x22, 0x32, 0x0a, 0x09, 0x46, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, + 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x52, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, + 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, + 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1346,48 +1653,51 @@ func file_users_proto_rawDescGZIP() []byte { return file_users_proto_rawDescData } -var file_users_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_users_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_users_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_users_proto_msgTypes = make([]protoimpl.MessageInfo, 16) var file_users_proto_goTypes = []interface{}{ - (GUISettings_UIMode)(0), // 0: proto.GUISettings.UIMode - (ApiGrrUser_UserType)(0), // 1: proto.ApiGrrUser.UserType - (UserNotification_Type)(0), // 2: proto.UserNotification.Type - (UserNotification_State)(0), // 3: proto.UserNotification.State - (*VelociraptorUser)(nil), // 4: proto.VelociraptorUser - (*GUISettings)(nil), // 5: proto.GUISettings - (*UILink)(nil), // 6: proto.UILink - (*ApiGrrUserInterfaceTraits)(nil), // 7: proto.ApiGrrUserInterfaceTraits - (*ApiGrrUser)(nil), // 8: proto.ApiGrrUser - (*UserNotificationCount)(nil), // 9: proto.UserNotificationCount - (*UserNotification)(nil), // 10: proto.UserNotification - (*GetUserNotificationsResponse)(nil), // 11: proto.GetUserNotificationsResponse - (*GetUserNotificationsRequest)(nil), // 12: proto.GetUserNotificationsRequest - (*SetGUIOptionsRequest)(nil), // 13: proto.SetGUIOptionsRequest - (*Users)(nil), // 14: proto.Users - (*Favorite)(nil), // 15: proto.Favorite - (*Favorites)(nil), // 16: proto.Favorites - (*proto.ApiClientACL)(nil), // 17: proto.ApiClientACL - (*proto1.ArtifactSpec)(nil), // 18: proto.ArtifactSpec + (ApiUser_UserType)(0), // 0: proto.ApiUser.UserType + (*Strings)(nil), // 1: proto.Strings + (*UserStats)(nil), // 2: proto.UserStats + (*VelociraptorUser)(nil), // 3: proto.VelociraptorUser + (*UpdateUserRequest)(nil), // 4: proto.UpdateUserRequest + (*DeleteUserRequest)(nil), // 5: proto.DeleteUserRequest + (*UserRequest)(nil), // 6: proto.UserRequest + (*ApiUserInterfaceTraits)(nil), // 7: proto.ApiUserInterfaceTraits + (*ApiUser)(nil), // 8: proto.ApiUser + (*GUICustomizations)(nil), // 9: proto.GUICustomizations + (*SetGUIOptionsRequest)(nil), // 10: proto.SetGUIOptionsRequest + (*SetGUIOptionsResponse)(nil), // 11: proto.SetGUIOptionsResponse + (*Users)(nil), // 12: proto.Users + (*UserRoles)(nil), // 13: proto.UserRoles + (*SetPasswordRequest)(nil), // 14: proto.SetPasswordRequest + (*Favorite)(nil), // 15: proto.Favorite + (*Favorites)(nil), // 16: proto.Favorites + (*proto.ApiClientACL)(nil), // 17: proto.ApiClientACL + (*OrgRecord)(nil), // 18: proto.OrgRecord + (*proto1.GUILink)(nil), // 19: proto.GUILink + (*proto2.ArtifactSpec)(nil), // 20: proto.ArtifactSpec } var file_users_proto_depIdxs = []int32{ 17, // 0: proto.VelociraptorUser.Permissions:type_name -> proto.ApiClientACL - 0, // 1: proto.GUISettings.mode:type_name -> proto.GUISettings.UIMode - 17, // 2: proto.ApiGrrUserInterfaceTraits.Permissions:type_name -> proto.ApiClientACL - 6, // 3: proto.ApiGrrUserInterfaceTraits.links:type_name -> proto.UILink - 5, // 4: proto.ApiGrrUser.settings:type_name -> proto.GUISettings - 7, // 5: proto.ApiGrrUser.interface_traits:type_name -> proto.ApiGrrUserInterfaceTraits - 1, // 6: proto.ApiGrrUser.user_type:type_name -> proto.ApiGrrUser.UserType - 2, // 7: proto.UserNotification.notification_type:type_name -> proto.UserNotification.Type - 3, // 8: proto.UserNotification.state:type_name -> proto.UserNotification.State - 10, // 9: proto.GetUserNotificationsResponse.items:type_name -> proto.UserNotification - 4, // 10: proto.Users.users:type_name -> proto.VelociraptorUser - 18, // 11: proto.Favorite.spec:type_name -> proto.ArtifactSpec - 15, // 12: proto.Favorites.items:type_name -> proto.Favorite - 13, // [13:13] is the sub-list for method output_type - 13, // [13:13] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 18, // 1: proto.VelociraptorUser.orgs:type_name -> proto.OrgRecord + 2, // 2: proto.VelociraptorUser.stats:type_name -> proto.UserStats + 17, // 3: proto.ApiUserInterfaceTraits.Permissions:type_name -> proto.ApiClientACL + 9, // 4: proto.ApiUserInterfaceTraits.customizations:type_name -> proto.GUICustomizations + 19, // 5: proto.ApiUserInterfaceTraits.links:type_name -> proto.GUILink + 7, // 6: proto.ApiUser.interface_traits:type_name -> proto.ApiUserInterfaceTraits + 0, // 7: proto.ApiUser.user_type:type_name -> proto.ApiUser.UserType + 18, // 8: proto.ApiUser.orgs:type_name -> proto.OrgRecord + 9, // 9: proto.SetGUIOptionsRequest.customizations:type_name -> proto.GUICustomizations + 19, // 10: proto.SetGUIOptionsRequest.links:type_name -> proto.GUILink + 3, // 11: proto.Users.users:type_name -> proto.VelociraptorUser + 20, // 12: proto.Favorite.spec:type_name -> proto.ArtifactSpec + 15, // 13: proto.Favorites.items:type_name -> proto.Favorite + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_users_proto_init() } @@ -1395,9 +1705,10 @@ func file_users_proto_init() { if File_users_proto != nil { return } + file_orgs_proto_init() if !protoimpl.UnsafeEnabled { file_users_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VelociraptorUser); i { + switch v := v.(*Strings); i { case 0: return &v.state case 1: @@ -1409,7 +1720,7 @@ func file_users_proto_init() { } } file_users_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GUISettings); i { + switch v := v.(*UserStats); i { case 0: return &v.state case 1: @@ -1421,7 +1732,7 @@ func file_users_proto_init() { } } file_users_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UILink); i { + switch v := v.(*VelociraptorUser); i { case 0: return &v.state case 1: @@ -1433,7 +1744,7 @@ func file_users_proto_init() { } } file_users_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApiGrrUserInterfaceTraits); i { + switch v := v.(*UpdateUserRequest); i { case 0: return &v.state case 1: @@ -1445,7 +1756,7 @@ func file_users_proto_init() { } } file_users_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApiGrrUser); i { + switch v := v.(*DeleteUserRequest); i { case 0: return &v.state case 1: @@ -1457,7 +1768,7 @@ func file_users_proto_init() { } } file_users_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UserNotificationCount); i { + switch v := v.(*UserRequest); i { case 0: return &v.state case 1: @@ -1469,7 +1780,7 @@ func file_users_proto_init() { } } file_users_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UserNotification); i { + switch v := v.(*ApiUserInterfaceTraits); i { case 0: return &v.state case 1: @@ -1481,7 +1792,7 @@ func file_users_proto_init() { } } file_users_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetUserNotificationsResponse); i { + switch v := v.(*ApiUser); i { case 0: return &v.state case 1: @@ -1493,7 +1804,7 @@ func file_users_proto_init() { } } file_users_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetUserNotificationsRequest); i { + switch v := v.(*GUICustomizations); i { case 0: return &v.state case 1: @@ -1517,7 +1828,7 @@ func file_users_proto_init() { } } file_users_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Users); i { + switch v := v.(*SetGUIOptionsResponse); i { case 0: return &v.state case 1: @@ -1529,7 +1840,7 @@ func file_users_proto_init() { } } file_users_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Favorite); i { + switch v := v.(*Users); i { case 0: return &v.state case 1: @@ -1541,6 +1852,42 @@ func file_users_proto_init() { } } file_users_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UserRoles); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_users_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetPasswordRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_users_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Favorite); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_users_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Favorites); i { case 0: return &v.state @@ -1558,8 +1905,8 @@ func file_users_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_users_proto_rawDesc, - NumEnums: 4, - NumMessages: 13, + NumEnums: 1, + NumMessages: 16, NumExtensions: 0, NumServices: 0, }, diff --git a/api/proto/users.proto b/api/proto/users.proto index 35cf28e1a..39c29b38c 100644 --- a/api/proto/users.proto +++ b/api/proto/users.proto @@ -2,12 +2,25 @@ syntax = "proto3"; import "proto/semantic.proto"; import "acls/proto/acl.proto"; +import "config/proto/config.proto"; import "flows/proto/artifact_collector.proto"; +import "orgs.proto"; + package proto; option go_package = "www.velocidex.com/golang/velociraptor/api/proto"; +message Strings { + repeated string strings = 1; +} + +message UserStats { + // Last time the user's record was fetched from the cache. + int64 last_active_time = 1; + string last_ip_address = 2; +} + message VelociraptorUser { string name = 1 [(sem_type) = { description: "The username" @@ -31,47 +44,67 @@ message VelociraptorUser { }]; ApiClientACL Permissions = 9; + + // A list of org id's the user belongs to. + repeated OrgRecord orgs = 11; + + // Only used by the GUI/API to determine the currently selected + // org the user wants to see. + string current_org = 12; + + UserStats stats = 13; } +message UpdateUserRequest { + string name = 1 [(sem_type) = { + description: "The username" + }]; -// Next field: 4 -message GUISettings { - option (semantic) = { - description: "User GUI settings and preferences." - }; + string password = 2 [(sem_type) = { + description: "The cleartext password" + }]; - enum UIMode { - BASIC = 0; - ADVANCED = 1; - DEBUG = 2; - } + repeated string orgs = 3; - UIMode mode = 1 [(sem_type) = { - description: "User interface mode.", - }]; + repeated string roles = 4; - bool canary_mode = 3 [(sem_type) = { - description: "If true, show features that are being canaried." - }]; + bool add_new_user = 5; +} + +message DeleteUserRequest { + string name = 1 [(sem_type) = { + description: "The username" + }]; + + repeated string orgs = 2 [(sem_type) = { + description: "The org IDs to remove this user (empty means all orgs)" + }]; } -message UILink { - string text = 1; - string url = 2; +message UserRequest { + string name = 1; + string org = 2; } -// These traits are used by the AdminUI Angular app to disable certain -// UI elements based on the user's permission set. -message ApiGrrUserInterfaceTraits { +// These traits are used to control the GUI App. Many of these fields +// are constructed from the VelociraptorUser, the config file, the +// GUIOptions etc. +message ApiUserInterfaceTraits { ApiClientACL Permissions = 9; + GUICustomizations customizations = 10; - bool auth_using_google = 19; + string lang = 22; + + // Set if the authenticator is password less (e.g. OAuth, SAML + // etc) + bool password_less = 25; string picture = 20; - repeated UILink links = 21; + repeated GUILink links = 21; // Get the user's preferred theme. string theme = 2; + string timezone = 23; // Downloads will be protected using this password. string default_password = 3; @@ -81,16 +114,29 @@ message ApiGrrUserInterfaceTraits { // An opaque setting object stored by the GUI. string ui_settings = 1; + + // Current selected Org. + string org = 24; + string org_name = 30; + + // The user's specific base path - only used for crazy reverse + // proxy configurations. + string base_path = 29; + + // Optional features of the UI to disable. + bool disable_server_events = 26; + string auth_redirect_template = 27; + bool disable_quarantine_button = 28; }; -message ApiGrrUser { +// Describe the user to the GUI. +message ApiUser { string username = 1 [(sem_type) = { description: "The name of the user." }]; - GUISettings settings = 2 [(sem_type) = { - description: "User UI settings." - }]; - ApiGrrUserInterfaceTraits interface_traits = 3 [(sem_type) = { + + // Gets constructed from the users.GetUserOptions() + ApiUserInterfaceTraits interface_traits = 3 [(sem_type) = { description: "User's interface traits (what they can and can't do " "in the UI)." }]; @@ -101,76 +147,95 @@ message ApiGrrUser { USER_TYPE_ADMIN = 2; } UserType user_type = 4; -} + repeated OrgRecord orgs = 11; + bool org_admin = 12 [(sem_type) = { + description: "Whether this user is an OrgAdmin" + }]; -message UserNotificationCount { - uint64 count = 1; + // Messages for the user are available. + int64 messages = 14; } -message UserNotification { - enum Type { - TYPE_UNSET = 0; - TYPE_CLIENT_INTERROGATED = 1; - TYPE_CLIENT_APPROVAL_REQUESTED = 2; - TYPE_HUNT_APPROVAL_REQUESTED = 3; - TYPE_CRON_JOB_APPROVAL_REQUESTED = 4; - TYPE_CLIENT_APPROVAL_GRANTED = 5; - TYPE_HUNT_APPROVAL_GRANTED = 6; - TYPE_CRON_JOB_APPROVAL_GRANTED = 7; - TYPE_VFS_FILE_COLLECTED = 8; - TYPE_VFS_FILE_COLLECTION_FAILED = 9; - TYPE_HUNT_STOPPED = 10; - TYPE_FILE_ARCHIVE_GENERATED = 11; - TYPE_FILE_ARCHIVE_GENERATION_FAILED = 12; - TYPE_FLOW_RUN_COMPLETED = 13; - TYPE_FLOW_RUN_FAILED = 14; - TYPE_VFS_LIST_DIRECTORY_COMPLETED = 15; - TYPE_VFS_RECURSIVE_LIST_DIRECTORY_COMPLETED = 16; - } - - enum State { - STATE_UNSET = 0; - STATE_PENDING = 1; - STATE_NOT_PENDING = 2; - } - - string username = 1; - Type notification_type = 2; - State state = 3; - uint64 timestamp = 4 [(sem_type) = { - type: "RDFDatetime" - }]; +// Contol the GUI per user. +message GUICustomizations { + bool disable_server_events = 1; + bool disable_user_management = 2; + bool disable_quarantine_button = 3; - string message = 5; -} + // Updated from config_obj.Defaults.HuntExpiryHours + int64 hunt_expiry_hours = 4; -message GetUserNotificationsResponse { - repeated UserNotification items = 1; -} - -message GetUserNotificationsRequest { - bool clear_pending = 1 [(sem_type) = { - description: "If set, clears pending notifications." - }]; + repeated string indexed_client_metadata = 5; } message SetGUIOptionsRequest { string theme = 2; + string timezone = 6; + string lang = 5; - // Downloads will be protected using this password. + // Downloads will be protected using this password. If this is + // empty we do not update the password. If it is set to "-" we + // reset the password to the empty string. string default_password = 3; // Offer to protect download exports by default. bool default_downloads_lock = 4; string options = 1; + + // Current org id + string org = 7; + + GUICustomizations customizations = 8; + + repeated GUILink links = 9; + + // Optional features of the UI to disable: + // TODO: Move to GUICustomizations. + bool disable_server_events = 26; + string auth_redirect_template = 27; + bool disable_quarantine_button = 28; + + // How many notifications are currently outstanding for the user. + int64 messages = 29; } +message SetGUIOptionsResponse { + // If this is specifed the GUI will redirect to the specified + // URL. This helps when there are multiple servers that can handle + // the user and a better one is needed. Normally this is really + // set by any middleware (e.g. custom authenticators). + string redirect_url = 1; +} + + message Users { repeated VelociraptorUser users = 1; } + +// Get the roles and permissions of a user within the org. +message UserRoles { + string name = 1; + string org = 2; + string org_name = 8; + + repeated string permissions = 3; + repeated string roles = 4; + + // Expanded permissions as above. + repeated string effective_permissions = 5; + + repeated string all_roles = 6; + repeated string all_permissions = 7; +}; + +message SetPasswordRequest { + string password = 1; + string username = 2; +} + // Store favorite collections (essential preset collection specs) message Favorite { string name = 1; @@ -182,4 +247,4 @@ message Favorite { message Favorites { repeated Favorite items = 1; -} \ No newline at end of file +} diff --git a/api/proto/vfs_api.pb.go b/api/proto/vfs_api.pb.go index 58084a735..b9b0dce0d 100644 --- a/api/proto/vfs_api.pb.go +++ b/api/proto/vfs_api.pb.go @@ -1,7 +1,4 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.12.4 // source: vfs_api.proto package proto @@ -21,6 +18,11 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// This message is written in the VFS datastore and contains metadata +// about the directory listing. Each protobuf refers to the files +// contained in a single directory. The actual file listing is stored +// in the flow's collection and this protobuf contains the range of +// rows within the collection that refers to this current directory. type VFSListResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -35,6 +37,14 @@ type VFSListResponse struct { // The actual artifact that contains the data. ClientId string `protobuf:"bytes,9,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` FlowId string `protobuf:"bytes,10,opt,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` + // The artifact name that contains the actual data for this + // directory. If not specified it will be "System.VFS.ListDirectory" + Artifact string `protobuf:"bytes,13,opt,name=artifact,proto3" json:"artifact,omitempty"` + StartIdx uint64 `protobuf:"varint,11,opt,name=start_idx,json=startIdx,proto3" json:"start_idx,omitempty"` + EndIdx uint64 `protobuf:"varint,12,opt,name=end_idx,json=endIdx,proto3" json:"end_idx,omitempty"` + // The version number that tracks the total download mutations in + // this directory. + DownloadVersion uint64 `protobuf:"varint,14,opt,name=download_version,json=downloadVersion,proto3" json:"download_version,omitempty"` } func (x *VFSListResponse) Reset() { @@ -125,6 +135,34 @@ func (x *VFSListResponse) GetFlowId() string { return "" } +func (x *VFSListResponse) GetArtifact() string { + if x != nil { + return x.Artifact + } + return "" +} + +func (x *VFSListResponse) GetStartIdx() uint64 { + if x != nil { + return x.StartIdx + } + return 0 +} + +func (x *VFSListResponse) GetEndIdx() uint64 { + if x != nil { + return x.EndIdx + } + return 0 +} + +func (x *VFSListResponse) GetDownloadVersion() uint64 { + if x != nil { + return x.DownloadVersion + } + return 0 +} + type VFSStatDownloadRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -369,13 +407,161 @@ func (x *VFSDownloadFileRequest) GetVfsComponents() []string { return nil } +type SearchFileRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + VfsComponents []string `protobuf:"bytes,2,rep,name=vfs_components,json=vfsComponents,proto3" json:"vfs_components,omitempty"` + // If true pad sparse files. + Padding bool `protobuf:"varint,7,opt,name=padding,proto3" json:"padding,omitempty"` + // The term to search for + Term string `protobuf:"bytes,3,opt,name=term,proto3" json:"term,omitempty"` + // The type of search term - default "string", "regex" + Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` + // Where to begin the search + Offset uint64 `protobuf:"varint,5,opt,name=offset,proto3" json:"offset,omitempty"` + // If true we search forward otherwise we search backwards from + // the offset + Forward bool `protobuf:"varint,6,opt,name=forward,proto3" json:"forward,omitempty"` +} + +func (x *SearchFileRequest) Reset() { + *x = SearchFileRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_vfs_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchFileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchFileRequest) ProtoMessage() {} + +func (x *SearchFileRequest) ProtoReflect() protoreflect.Message { + mi := &file_vfs_api_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchFileRequest.ProtoReflect.Descriptor instead. +func (*SearchFileRequest) Descriptor() ([]byte, []int) { + return file_vfs_api_proto_rawDescGZIP(), []int{5} +} + +func (x *SearchFileRequest) GetVfsComponents() []string { + if x != nil { + return x.VfsComponents + } + return nil +} + +func (x *SearchFileRequest) GetPadding() bool { + if x != nil { + return x.Padding + } + return false +} + +func (x *SearchFileRequest) GetTerm() string { + if x != nil { + return x.Term + } + return "" +} + +func (x *SearchFileRequest) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *SearchFileRequest) GetOffset() uint64 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *SearchFileRequest) GetForward() bool { + if x != nil { + return x.Forward + } + return false +} + +type SearchFileResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + VfsComponents []string `protobuf:"bytes,2,rep,name=vfs_components,json=vfsComponents,proto3" json:"vfs_components,omitempty"` + Hit uint64 `protobuf:"varint,3,opt,name=hit,proto3" json:"hit,omitempty"` +} + +func (x *SearchFileResponse) Reset() { + *x = SearchFileResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_vfs_api_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchFileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchFileResponse) ProtoMessage() {} + +func (x *SearchFileResponse) ProtoReflect() protoreflect.Message { + mi := &file_vfs_api_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchFileResponse.ProtoReflect.Descriptor instead. +func (*SearchFileResponse) Descriptor() ([]byte, []int) { + return file_vfs_api_proto_rawDescGZIP(), []int{6} +} + +func (x *SearchFileResponse) GetVfsComponents() []string { + if x != nil { + return x.VfsComponents + } + return nil +} + +func (x *SearchFileResponse) GetHit() uint64 { + if x != nil { + return x.Hit + } + return 0 +} + var File_vfs_api_proto protoreflect.FileDescriptor var file_vfs_api_proto_rawDesc = []byte{ 0x0a, 0x0d, 0x76, 0x66, 0x73, 0x5f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x71, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, - 0x8c, 0x02, 0x0a, 0x0f, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x89, 0x03, 0x0a, 0x0f, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, @@ -391,40 +577,64 @@ var file_vfs_api_proto_rawDesc = []byte{ 0x70, 0x52, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x22, 0x71, - 0x0a, 0x16, 0x56, 0x46, 0x53, 0x53, 0x74, 0x61, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, - 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x1a, + 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x78, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x49, 0x64, 0x78, 0x12, 0x17, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x5f, 0x69, + 0x64, 0x78, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x78, + 0x12, 0x29, 0x0a, 0x10, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x64, 0x6f, 0x77, 0x6e, + 0x6c, 0x6f, 0x61, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x71, 0x0a, 0x16, 0x56, + 0x46, 0x53, 0x53, 0x74, 0x61, 0x74, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, 0x1e, + 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x7d, + 0x0a, 0x0e, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x27, 0x0a, + 0x0f, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x65, 0x70, 0x74, 0x68, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x44, 0x65, 0x70, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x66, 0x73, 0x5f, 0x63, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, + 0x76, 0x66, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x7f, 0x0a, + 0x13, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, + 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, + 0x12, 0x2c, 0x0a, 0x07, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x51, 0x4c, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x07, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x22, 0x5c, + 0x0a, 0x16, 0x56, 0x46, 0x53, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, - 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, - 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, - 0x73, 0x22, 0x7d, 0x0a, 0x0e, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, - 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x65, - 0x70, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x72, 0x65, 0x63, 0x75, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x70, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x66, 0x73, - 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x0d, 0x76, 0x66, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, - 0x22, 0x7f, 0x0a, 0x13, 0x56, 0x46, 0x53, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, - 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, - 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x07, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x56, 0x51, 0x4c, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x07, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, - 0x74, 0x22, 0x5c, 0x0a, 0x16, 0x56, 0x46, 0x53, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, - 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x66, 0x73, 0x5f, - 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0d, 0x76, 0x66, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x42, - 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, - 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x66, 0x73, 0x5f, 0x63, 0x6f, 0x6d, + 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x76, + 0x66, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x22, 0xae, 0x01, 0x0a, + 0x11, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x66, 0x73, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, + 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x76, 0x66, 0x73, 0x43, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x64, + 0x64, 0x69, 0x6e, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x61, 0x64, 0x64, + 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, + 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, + 0x73, 0x65, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x22, 0x4d, 0x0a, + 0x12, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x66, 0x73, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, + 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x76, 0x66, 0x73, + 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x68, 0x69, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x68, 0x69, 0x74, 0x42, 0x31, 0x5a, 0x2f, + 0x77, 0x77, 0x77, 0x2e, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, + 0x61, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -439,21 +649,23 @@ func file_vfs_api_proto_rawDescGZIP() []byte { return file_vfs_api_proto_rawDescData } -var file_vfs_api_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_vfs_api_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_vfs_api_proto_goTypes = []interface{}{ (*VFSListResponse)(nil), // 0: proto.VFSListResponse (*VFSStatDownloadRequest)(nil), // 1: proto.VFSStatDownloadRequest (*VFSListRequest)(nil), // 2: proto.VFSListRequest (*VFSListRequestState)(nil), // 3: proto.VFSListRequestState (*VFSDownloadFileRequest)(nil), // 4: proto.VFSDownloadFileRequest - (*proto.VQLRequest)(nil), // 5: proto.VQLRequest - (*proto.VQLTypeMap)(nil), // 6: proto.VQLTypeMap - (*proto.VQLResponse)(nil), // 7: proto.VQLResponse + (*SearchFileRequest)(nil), // 5: proto.SearchFileRequest + (*SearchFileResponse)(nil), // 6: proto.SearchFileResponse + (*proto.VQLRequest)(nil), // 7: proto.VQLRequest + (*proto.VQLTypeMap)(nil), // 8: proto.VQLTypeMap + (*proto.VQLResponse)(nil), // 9: proto.VQLResponse } var file_vfs_api_proto_depIdxs = []int32{ - 5, // 0: proto.VFSListResponse.Query:type_name -> proto.VQLRequest - 6, // 1: proto.VFSListResponse.types:type_name -> proto.VQLTypeMap - 7, // 2: proto.VFSListRequestState.current:type_name -> proto.VQLResponse + 7, // 0: proto.VFSListResponse.Query:type_name -> proto.VQLRequest + 8, // 1: proto.VFSListResponse.types:type_name -> proto.VQLTypeMap + 9, // 2: proto.VFSListRequestState.current:type_name -> proto.VQLResponse 3, // [3:3] is the sub-list for method output_type 3, // [3:3] is the sub-list for method input_type 3, // [3:3] is the sub-list for extension type_name @@ -527,6 +739,30 @@ func file_vfs_api_proto_init() { return nil } } + file_vfs_api_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SearchFileRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_vfs_api_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SearchFileResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -534,7 +770,7 @@ func file_vfs_api_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_vfs_api_proto_rawDesc, NumEnums: 0, - NumMessages: 5, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, diff --git a/api/proto/vfs_api.proto b/api/proto/vfs_api.proto index 135986d03..74fd12b23 100644 --- a/api/proto/vfs_api.proto +++ b/api/proto/vfs_api.proto @@ -8,6 +8,11 @@ option go_package = "www.velocidex.com/golang/velociraptor/api/proto"; // Messages to interact with the API +// This message is written in the VFS datastore and contains metadata +// about the directory listing. Each protobuf refers to the files +// contained in a single directory. The actual file listing is stored +// in the flow's collection and this protobuf contains the range of +// rows within the collection that refers to this current directory. message VFSListResponse { string Response = 1; repeated string Columns = 2; @@ -19,6 +24,16 @@ message VFSListResponse { // The actual artifact that contains the data. string client_id = 9; string flow_id = 10; + + // The artifact name that contains the actual data for this + // directory. If not specified it will be "System.VFS.ListDirectory" + string artifact = 13; + uint64 start_idx = 11; + uint64 end_idx = 12; + + // The version number that tracks the total download mutations in + // this directory. + uint64 download_version = 14; } message VFSStatDownloadRequest { @@ -45,3 +60,30 @@ message VFSDownloadFileRequest { repeated string vfs_components = 2; } + + +message SearchFileRequest { + repeated string vfs_components = 2; + + // If true pad sparse files. + bool padding = 7; + + // The term to search for + string term = 3; + + // The type of search term - default "string", "regex" + string type = 4; + + // Where to begin the search + uint64 offset = 5; + + // If true we search forward otherwise we search backwards from + // the offset + bool forward = 6; +} + +message SearchFileResponse { + repeated string vfs_components = 2; + + uint64 hit = 3; +} \ No newline at end of file diff --git a/api/proxy.go b/api/proxy.go index 5ce3749cd..6509a2a0b 100644 --- a/api/proxy.go +++ b/api/proxy.go @@ -1,50 +1,53 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package api import ( + "context" "crypto/tls" "crypto/x509" + "fmt" "net/http" "net/http/httputil" "net/url" - "strings" + errors "github.com/go-errors/errors" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - errors "github.com/pkg/errors" - "golang.org/x/net/context" + "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/encoding/protojson" + "www.velocidex.com/golang/velociraptor/acls" "www.velocidex.com/golang/velociraptor/api/authenticators" api_proto "www.velocidex.com/golang/velociraptor/api/proto" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/constants" crypto_utils "www.velocidex.com/golang/velociraptor/crypto/utils" - file_store "www.velocidex.com/golang/velociraptor/file_store" - "www.velocidex.com/golang/velociraptor/file_store/accessors" "www.velocidex.com/golang/velociraptor/grpc_client" "www.velocidex.com/golang/velociraptor/logging" + debug_server "www.velocidex.com/golang/velociraptor/services/debug/server" + "www.velocidex.com/golang/velociraptor/utils" ) // A Mux for the reverse proxy feature. -func AddProxyMux(config_obj *config_proto.Config, mux *http.ServeMux) error { +func AddProxyMux(config_obj *config_proto.Config, mux *api_utils.ServeMux) error { if config_obj.GUI == nil { return errors.New("GUI not configured") } @@ -62,28 +65,29 @@ func AddProxyMux(config_obj *config_proto.Config, mux *http.ServeMux) error { var handler http.Handler if target.Scheme == "file" { - handler = http.StripPrefix(reverse_proxy_config.Route, + handler = api_utils.StripPrefix(reverse_proxy_config.Route, http.FileServer(http.Dir(target.Path))) } else { - handler = http.StripPrefix(reverse_proxy_config.Route, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.URL.Host = target.Host - r.URL.Scheme = target.Scheme - r.Header.Set("X-Forwarded-Host", r.Header.Get("Host")) - r.Host = target.Host - - // If we require auth we do - // not pass the auth header to - // the target of the - // proxy. Otherwise we leave - // authentication to it. - if reverse_proxy_config.RequireAuth { - r.Header.Del("Authorization") - } - - httputil.NewSingleHostReverseProxy(target).ServeHTTP(w, r) - })) + handler = api_utils.StripPrefix(reverse_proxy_config.Route, + api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + r.URL.Host = target.Host + r.URL.Scheme = target.Scheme + r.Header.Set("X-Forwarded-Host", r.Header.Get("Host")) + r.Host = target.Host + + // If we require auth we do + // not pass the auth header to + // the target of the + // proxy. Otherwise we leave + // authentication to it. + if reverse_proxy_config.RequireAuth { + r.Header.Del("Authorization") + } + + httputil.NewSingleHostReverseProxy(target).ServeHTTP(w, r) + })) } if reverse_proxy_config.RequireAuth { @@ -91,7 +95,8 @@ func AddProxyMux(config_obj *config_proto.Config, mux *http.ServeMux) error { if err != nil { return err } - handler = auther.AuthenticateUserHandler(config_obj, handler) + // Minimum level of access should be READ_RESULTS + handler = auther.AuthenticateUserHandler(handler, acls.READ_RESULTS) } mux.Handle(reverse_proxy_config.Route, handler) @@ -103,7 +108,8 @@ func AddProxyMux(config_obj *config_proto.Config, mux *http.ServeMux) error { // Prepares a mux for the GUI by adding handlers required by the GUI. func PrepareGUIMux( ctx context.Context, - config_obj *config_proto.Config, mux *http.ServeMux) (http.Handler, error) { + config_obj *config_proto.Config, + mux *api_utils.ServeMux) (http.Handler, error) { if config_obj.GUI == nil { return nil, errors.New("GUI not configured") } @@ -122,53 +128,95 @@ func PrepareGUIMux( if err != nil { return nil, err } + if config_obj.GUI != nil && config_obj.GUI.Authenticator != nil { + logger := logging.GetLogger(config_obj, &logging.GUIComponent) + logger.Info("GUI will use the %v authenticator", config_obj.GUI.Authenticator.Type) + } + + // Add the authenticator specific handlers. + err = auther.AddHandlers(mux) + if err != nil { + return nil, err + } - err = auther.AddHandlers(config_obj, mux) + // Add the logout handlers + err = auther.AddLogoff(mux) if err != nil { return nil, err } - base := config_obj.GUI.BasePath + base_path := api_utils.GetBasePath(config_obj) - mux.Handle(base+"/api/", csrfProtect(config_obj, - auther.AuthenticateUserHandler(config_obj, h))) + mux.Handle(api_utils.GetBasePath(config_obj, "/api/"), + ipFilter(config_obj, + csrfProtect(config_obj, + auther.AuthenticateUserHandler(h, acls.READ_RESULTS)))) - mux.Handle(base+"/api/v1/DownloadTable", csrfProtect(config_obj, - auther.AuthenticateUserHandler( - config_obj, downloadTable(config_obj)))) + mux.Handle(api_utils.GetBasePath(config_obj, "/api/v1/DownloadTable"), + ipFilter(config_obj, csrfProtect(config_obj, + auther.AuthenticateUserHandler( + downloadTable(config_obj), acls.READ_RESULTS)))) - mux.Handle(base+"/api/v1/DownloadVFSFile", csrfProtect(config_obj, - auther.AuthenticateUserHandler( - config_obj, vfsFileDownloadHandler(config_obj)))) + mux.Handle(api_utils.GetBasePath(config_obj, "/api/v1/DownloadVFSFile"), + ipFilter(config_obj, csrfProtect(config_obj, + auther.AuthenticateUserHandler( + vfsFileDownloadHandler(config_obj), acls.READ_RESULTS)))) - mux.Handle(base+"/api/v1/UploadTool", csrfProtect(config_obj, - auther.AuthenticateUserHandler( - config_obj, toolUploadHandler(config_obj)))) + mux.Handle(api_utils.GetBasePath(config_obj, "/api/v1/UploadTool"), + ipFilter(config_obj, csrfProtect(config_obj, + auther.AuthenticateUserHandler( + toolUploadHandler(config_obj), acls.READ_RESULTS)))) - mux.Handle(base+"/api/v1/UploadFormFile", csrfProtect(config_obj, - auther.AuthenticateUserHandler( - config_obj, formUploadHandler(config_obj)))) + mux.Handle(api_utils.GetBasePath(config_obj, "/api/v1/UploadFormFile"), + ipFilter(config_obj, csrfProtect(config_obj, + auther.AuthenticateUserHandler( + formUploadHandler(config_obj), acls.READ_RESULTS)))) // Serve prepared zip files. - mux.Handle(base+"/downloads/", csrfProtect(config_obj, - auther.AuthenticateUserHandler( - config_obj, http.StripPrefix(base, forceMime(http.FileServer( - accessors.NewFileSystem( - config_obj, - file_store.GetFileStore(config_obj), - "/downloads/"))))))) + mux.Handle(api_utils.GetBasePath(config_obj, "/downloads/"), + ipFilter(config_obj, csrfProtect(config_obj, + auther.AuthenticateUserHandler( + api_utils.StripPrefix(base_path, + downloadFileStore(config_obj, []string{"downloads"})), + acls.READ_RESULTS)))) // Serve notebook items - mux.Handle(base+"/notebooks/", csrfProtect(config_obj, - auther.AuthenticateUserHandler( - config_obj, http.StripPrefix(base, forceMime(http.FileServer( - accessors.NewFileSystem( - config_obj, - file_store.GetFileStore(config_obj), - "/notebooks/"))))))) + mux.Handle(api_utils.GetBasePath(config_obj, "/notebooks/"), + ipFilter(config_obj, csrfProtect(config_obj, + auther.AuthenticateUserHandler( + api_utils.StripPrefix(base_path, + downloadFileStore(config_obj, []string{"notebooks"})), + acls.READ_RESULTS)))) + + // Serve files from hunt notebooks + mux.Handle(api_utils.GetBasePath(config_obj, "/hunts/"), + ipFilter(config_obj, csrfProtect(config_obj, + auther.AuthenticateUserHandler( + api_utils.StripPrefix(base_path, + downloadFileStore(config_obj, []string{"hunts"})), + acls.READ_RESULTS)))) + + // Serve files from client notebooks + mux.Handle(api_utils.GetBasePath(config_obj, "/clients/"), + ipFilter(config_obj, csrfProtect(config_obj, + auther.AuthenticateUserHandler( + api_utils.StripPrefix(base_path, + downloadFileStore(config_obj, []string{"clients"})), + acls.READ_RESULTS)))) + + // Enable debug endpoints but only for users with SERVER_ADMIN on + // the root org, because the debug server currently exposes all + // org's data. The debug server requires access to the root org! + mux.Handle(api_utils.GetBasePath(config_obj, "/debug/"), + ipFilter(config_obj, csrfProtect(config_obj, + auther.AuthenticateUserHandler( + api_utils.StripPrefix(base_path, + debug_server.DebugMux(config_obj, base_path). + RequireRootOrg()), + acls.SERVER_ADMIN)))) // Assets etc do not need auth. - install_static_assets(config_obj, mux) + install_static_assets(ctx, config_obj, mux) // Add reverse proxy support. err = AddProxyMux(config_obj, mux) @@ -180,29 +228,27 @@ func PrepareGUIMux( if err != nil { return nil, err } - mux.Handle(base+"/app/index.html", csrfProtect(config_obj, - auther.AuthenticateUserHandler(config_obj, h))) - - mux.Handle(base+"/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, base+"/app/index.html", 302) - })) + mux.Handle(api_utils.GetBasePath(config_obj, "/app/index.html"), + ipFilter(config_obj, + csrfProtect(config_obj, + auther.AuthenticateUserHandler(h, acls.READ_RESULTS)))) + + // Redirect everything else to the app + mux.Handle(api_utils.GetBaseDirectory(config_obj), + api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, + api_utils.GetBasePath(config_obj, "/app/index.html"), + http.StatusTemporaryRedirect) + })) return mux, nil } -type _templateArgs struct { - Timestamp int64 - Heading string - Help_url string - Report_url string - Version string - CsrfToken string - BasePath string - UserTheme string -} - // An api handler which connects to the gRPC service (i.e. it is a -// gRPC client). +// gRPC client). This is used by the gRPC gateway to relay REST calls +// to the gRPC API. This connection must be identified as the gateway +// identity. func GetAPIHandler( ctx context.Context, config_obj *config_proto.Config) (http.Handler, error) { @@ -233,7 +279,9 @@ func GetAPIHandler( username, ok := req.Context().Value( constants.GRPC_USER_CONTEXT).(string) if ok { - md["USER"] = username + // gRPC metadata can only contain ASCII so we make + // sure to escape if needed. + md["USER"] = utils.Quote(username) } return metadata.New(md) @@ -262,24 +310,35 @@ func GetAPIHandler( _, err = gw_cert.Verify(x509.VerifyOptions{Roots: CA_Pool}) if err != nil { - return nil, errors.WithStack(err) + return nil, errors.Wrap(err, 0) } gw_name := crypto_utils.GetSubjectName(gw_cert) - if gw_name != config_obj.API.PinnedGwName { - return nil, errors.New("GUI gRPC proxy Certificate is not correct") + if gw_name != utils.GetGatewayName(config_obj) { + return nil, fmt.Errorf( + "GUI gRPC proxy Certificate is not correct: %v", gw_name) } + // The API server's TLS address is pinned to the frontend's + // certificate. We must only connect to the real API server. creds := credentials.NewTLS(&tls.Config{ Certificates: []tls.Certificate{cert}, RootCAs: CA_Pool, - ServerName: config_obj.Client.PinnedServerName, + ServerName: utils.GetSuperuserName(config_obj), }) opts := []grpc.DialOption{ grpc.WithTransportCredentials(creds), } + // Allow the receive limit to be increased. + if config_obj.ApiConfig != nil && + config_obj.ApiConfig.MaxGrpcRecvSize > 0 { + opts = append(opts, + grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize( + int(config_obj.ApiConfig.MaxGrpcRecvSize)))) + } + bind_addr := grpc_client.GetAPIConnectionString(config_obj) err = api_proto.RegisterAPIHandlerFromEndpoint( ctx, grpc_proxy_mux, bind_addr, opts) @@ -287,25 +346,15 @@ func GetAPIHandler( return nil, err } - base := config_obj.GUI.BasePath - - reverse_proxy_mux := http.NewServeMux() - reverse_proxy_mux.Handle(base+"/api/v1/", - http.StripPrefix(base, grpc_proxy_mux)) + reverse_proxy_mux := api_utils.NewServeMux() + reverse_proxy_mux.Handle(api_utils.GetBasePath(config_obj, "/api/v1/"), + api_utils.StripPrefix( + api_utils.GetBasePath(config_obj), grpc_proxy_mux)) return reverse_proxy_mux, nil } -// Force mime type to binary stream. -func forceMime(parent http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Prevent directory listings. - if strings.HasSuffix(r.URL.Path, "/") { - http.NotFound(w, r) - return - } - - w.Header().Set("Content-Type", "binary/octet-stream") - parent.ServeHTTP(w, r) - }) +func ipFilter(config_obj *config_proto.Config, + parent http.Handler) http.Handler { + return authenticators.IpFilter(config_obj, parent) } diff --git a/api/proxy_test.go b/api/proxy_test.go new file mode 100644 index 000000000..3d7426960 --- /dev/null +++ b/api/proxy_test.go @@ -0,0 +1,105 @@ +package api + +import ( + "fmt" + "testing" + + "github.com/Velocidex/ordereddict" + "github.com/stretchr/testify/suite" + "google.golang.org/protobuf/proto" + "www.velocidex.com/golang/velociraptor/api/authenticators" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/file_store/test_utils" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/vtesting/assert" + "www.velocidex.com/golang/velociraptor/vtesting/goldie" +) + +type APIProxyTestSuite struct { + test_utils.TestSuite +} + +func (self *APIProxyTestSuite) TestMultiAuthenticator() { + authenticators.ResetAuthCache() + + mux := api_utils.NewServeMux() + + config_obj := proto.Clone(self.ConfigObj).(*config_proto.Config) + config_obj.GUI.PublicUrl = "https://www.example.com/" + config_obj.GUI.BasePath = "/velociraptor" + config_obj.GUI.Authenticator = &config_proto.Authenticator{ + Type: "multi", + SubAuthenticators: []*config_proto.Authenticator{{ + Type: "oidc", + OidcIssuer: "https://accounts.google.com", + OauthClientId: "CCCCC", + OauthClientSecret: "secret", + }, { + Type: "Google", + OauthClientId: "CCCCC", + OauthClientSecret: "secret", + }, { + Type: "GitHub", + OauthClientId: "CCCCC", + OauthClientSecret: "secret", + }, { + Type: "azure", + OauthClientId: "CCCCC", + OauthClientSecret: "secret", + }}, + } + + _, err := PrepareGUIMux(self.Ctx, config_obj, mux) + assert.NoError(self.T(), err) + + auther, err := authenticators.NewAuthenticator(config_obj) + assert.NoError(self.T(), err) + + auther_multi, ok := auther.(*authenticators.MultiAuthenticator) + assert.True(self.T(), ok) + + golden := ordereddict.NewDict() + + for _, delegate := range auther_multi.Delegates() { + auther_oidc, ok := delegate.(*authenticators.OidcAuthenticator) + if !ok { + continue + } + + oidc_config, err := auther_oidc.GetGenOauthConfig() + assert.NoError(self.T(), err) + golden.Set(fmt.Sprintf("Redirect Provider %T %v", delegate, auther_oidc.Name()), + oidc_config.RedirectURL) + } + + golden.Set("Mux", mux.Debug()) + + goldie.Assert(self.T(), "TestMultiAuthenticator", json.MustMarshalIndent(golden)) +} + +func (self *APIProxyTestSuite) TestBasicAuthenticator() { + authenticators.ResetAuthCache() + + mux := api_utils.NewServeMux() + + config_obj := proto.Clone(self.ConfigObj).(*config_proto.Config) + config_obj.GUI.PublicUrl = "https://www.example.com/" + config_obj.GUI.BasePath = "/velociraptor" + config_obj.GUI.Authenticator = &config_proto.Authenticator{ + Type: "basic", + } + + _, err := PrepareGUIMux(self.Ctx, config_obj, mux) + assert.NoError(self.T(), err) + + golden := ordereddict.NewDict() + + golden.Set("Mux", mux.Debug()) + + goldie.Assert(self.T(), "TestBasicAuthenticator", json.MustMarshalIndent(golden)) +} + +func TestAPIProxy(t *testing.T) { + suite.Run(t, &APIProxyTestSuite{}) +} diff --git a/api/query.go b/api/query.go index f55ae0b98..87cbfc502 100644 --- a/api/query.go +++ b/api/query.go @@ -1,40 +1,45 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package api import ( + "context" "fmt" "io" "log" "runtime/debug" + "sync" "time" "github.com/Velocidex/ordereddict" "github.com/dustin/go-humanize" - errors "github.com/pkg/errors" + errors "github.com/go-errors/errors" + "github.com/sirupsen/logrus" - context "golang.org/x/net/context" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" api_proto "www.velocidex.com/golang/velociraptor/api/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/executor/throttler" "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" "www.velocidex.com/golang/vfilter" ) @@ -48,6 +53,7 @@ func streamQuery( logger := logging.GetLogger(config_obj, &logging.APICmponent) logger.WithFields(logrus.Fields{ "arg": arg, + "org": arg.OrgId, "user": peer_name, }).Info("Query API call") @@ -55,11 +61,6 @@ func streamQuery( arg.MaxWait = 10 } - rate := arg.OpsPerSecond - if rate == 0 { - rate = 1000000 - } - if arg.Query == nil { return errors.New("Query should be specified.") } @@ -73,13 +74,17 @@ func streamQuery( }() response_channel := make(chan *actions_proto.VQLResponse) - scope_logger := MakeLogger(ctx, response_channel) + sub_ctx, cancel := context.WithCancel(ctx) + defer cancel() + + scope_logger := MakeLogger(sub_ctx, response_channel) // Add extra artifacts to the query from the global repository. - manager, err := services.GetRepositoryManager() + manager, err := services.GetRepositoryManager(config_obj) if err != nil { return err } + repository, err := manager.GetGlobalRepository(config_obj) if err != nil { return err @@ -87,7 +92,7 @@ func streamQuery( builder := services.ScopeBuilder{ Config: config_obj, - ACLManager: vql_subsystem.NewServerACLManager(config_obj, peer_name), + ACLManager: acl_managers.NewServerACLManager(config_obj, peer_name), Logger: scope_logger, Repository: repository, Env: ordereddict.NewDict(), @@ -100,12 +105,54 @@ func streamQuery( // Now execute the query. scope := manager.BuildScope(builder) - // Throttle the query if required. - vfilter.InstallThrottler(scope, vfilter.NewTimeThrottler(float64(rate))) + // Keep the connection open until the logs are sent. + wg := sync.WaitGroup{} + defer wg.Wait() + + // Implement timeout + if arg.Timeout > 0 { + start := time.Now() + timed_ctx, timed_cancel := utils.WithTimeoutCause(sub_ctx, + time.Second*time.Duration(arg.Timeout), + errors.New("Query API timeout reached")) + + wg.Add(1) + go func() { + defer wg.Done() + + select { + // Cancelling the parent will not return a log. + case <-sub_ctx.Done(): + timed_cancel() + + // Log the timeout + case <-timed_ctx.Done(): + scope.Log("collect: Timeout Error: Collection timed out after %v", + utils.GetTime().Now().Sub(start)) + // Cancel the main context. + cancel() + timed_cancel() + } + }() + } + wg.Add(1) go func() { + defer wg.Done() defer close(response_channel) defer scope.Close() + defer cancel() + + // Throttle the query if required. This must run in a + // goroutine so it can emit logs otherwise we deadlock! + t, closer := throttler.NewThrottler(sub_ctx, scope, config_obj, + 0, float64(arg.CpuLimit), 0) + scope.SetThrottler(t) + err = scope.AddDestructor(closer) + if err != nil { + closer() + return + } scope.Log("Starting query execution.") @@ -121,10 +168,11 @@ func streamQuery( // All the queries will use the same scope. This allows one // query to define functions for the next query in order. for query_idx, vql := range statements { - logger.Info("Query: Running %v\n", vql.ToString(scope)) + logger.Info("Query: Running %v\n", + vfilter.FormatToString(scope, vql)) result_chan := vfilter.GetResponseChannel( - vql, stream.Context(), scope, + vql, sub_ctx, scope, vql_subsystem.MarshalJson(scope), int(arg.MaxRow), int(arg.MaxWait)) @@ -174,6 +222,16 @@ type logWriter struct { } func (self *logWriter) Write(b []byte) (int, error) { + // Sometimes the channel becomes closed for some reason and this + // tends to panic. + defer utils.CheckForPanic("logWriter.Write") + + select { + case <-self.ctx.Done(): + return 0, io.EOF + default: + } + select { case <-self.ctx.Done(): return 0, io.EOF @@ -188,5 +246,5 @@ func (self *logWriter) Write(b []byte) (int, error) { func MakeLogger(ctx context.Context, output chan *actions_proto.VQLResponse) *log.Logger { result := &logWriter{output: output, ctx: ctx} - return log.New(result, "vql: ", 0) + return log.New(result, "", 0) } diff --git a/api/reflect.go b/api/reflect.go index 5c28992fc..f90ee5fcb 100644 --- a/api/reflect.go +++ b/api/reflect.go @@ -1,54 +1,71 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package api import ( + "context" "regexp" "strings" + "sync" - "github.com/Velocidex/yaml/v2" - context "golang.org/x/net/context" "google.golang.org/protobuf/types/known/emptypb" api_proto "www.velocidex.com/golang/velociraptor/api/proto" - "www.velocidex.com/golang/velociraptor/artifacts/assets" artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/types" ) var ( - doc_regex = regexp.MustCompile("doc=(.+)") + doc_regex = regexp.MustCompile("doc=(.+)") + mu sync.Mutex + cachedDescriptions []*api_proto.Completion ) -// Loads the api description from the embedded asset -func LoadApiDescription() ([]*api_proto.Completion, error) { - assets.Init() +// Get the top level description line. +func elideDescription(in string) string { + parts := strings.SplitN(in, ".", 2) + return utils.Elide(parts[0], 80) +} + +func loadApiDescriptions() []*api_proto.Completion { + mu.Lock() + defer mu.Unlock() + + if len(cachedDescriptions) > 0 { + return cachedDescriptions + } - data, err := assets.ReadFile("docs/references/vql.yaml") + descriptions, err := utils.LoadApiDescription() if err != nil { - return nil, err + descriptions = IntrospectDescription() } - result := []*api_proto.Completion{} - err = yaml.Unmarshal(data, &result) - return result, err + for _, d := range descriptions { + d.Description = elideDescription(d.Description) + } + + // Cache it for next time. + cachedDescriptions = descriptions + + return cachedDescriptions } func IntrospectDescription() []*api_proto.Completion { @@ -61,20 +78,36 @@ func IntrospectDescription() []*api_proto.Completion { info := scope.Describe(type_map) for _, item := range info.Functions { + var metadata map[string]string + if item.Metadata != nil { + metadata = make(map[string]string) + for _, i := range item.Metadata.Items() { + metadata[i.Key] = utils.ToString(i.Value) + } + } result = append(result, &api_proto.Completion{ Name: item.Name, - Description: item.Doc, + Description: elideDescription(item.Doc), Type: "Function", Args: getArgDescriptors(item.ArgType, type_map, scope), + Metadata: metadata, }) } for _, item := range info.Plugins { + var metadata map[string]string + if item.Metadata != nil { + metadata = make(map[string]string) + for _, i := range item.Metadata.Items() { + metadata[i.Key] = utils.ToString(i.Value) + } + } result = append(result, &api_proto.Completion{ Name: item.Name, - Description: item.Doc, + Description: elideDescription(item.Doc), Type: "Plugin", Args: getArgDescriptors(item.ArgType, type_map, scope), + Metadata: metadata, }) } @@ -85,8 +118,17 @@ func (self *ApiServer) GetKeywordCompletions( ctx context.Context, in *emptypb.Empty) (*api_proto.KeywordCompletions, error) { + defer Instrument("GetKeywordCompletions")() + + users := services.GetUserManager() + _, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + result := &api_proto.KeywordCompletions{ Items: []*api_proto.Completion{ + {Name: "EXPLAIN", Type: "Keyword"}, {Name: "SELECT", Type: "Keyword"}, {Name: "FROM", Type: "Keyword"}, {Name: "LET", Type: "Keyword"}, @@ -94,33 +136,34 @@ func (self *ApiServer) GetKeywordCompletions( {Name: "LIMIT", Type: "Keyword"}, {Name: "GROUP BY", Type: "Keyword"}, {Name: "ORDER BY", Type: "Keyword"}, + {Name: "DESC", Type: "Keyword"}, }, } - descriptions, err := LoadApiDescription() + result.Items = append(result.Items, loadApiDescriptions()...) + + manager, err := services.GetRepositoryManager(org_config_obj) if err != nil { - descriptions = IntrospectDescription() + return nil, Status(self.verbose, err) } - result.Items = append(result.Items, descriptions...) - - manager, err := services.GetRepositoryManager() + repository, err := manager.GetGlobalRepository(org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - repository, err := manager.GetGlobalRepository(self.config) + names, err := repository.List(ctx, org_config_obj) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - for _, name := range repository.List() { - artifact, pres := repository.Get(self.config, name) + + for _, name := range names { + artifact, pres := repository.Get(ctx, org_config_obj, name) if !pres { continue } result.Items = append(result.Items, &api_proto.Completion{ - Name: "Artifact." + name, - Type: "Artifact", - Description: artifact.Description, - Args: getArtifactParamDescriptors(artifact), + Name: "Artifact." + name, + Type: "Artifact", + Args: getArtifactParamDescriptors(artifact), }) } @@ -134,9 +177,8 @@ func getArgDescriptors( args := []*api_proto.ArgDescriptor{} arg_desc, pres := type_map.Get(scope, arg_type) if pres && arg_desc != nil && arg_desc.Fields != nil { - for _, k := range arg_desc.Fields.Keys() { - v_any, _ := arg_desc.Fields.Get(k) - v, ok := v_any.(*types.TypeReference) + for _, i := range arg_desc.Fields.Items() { + v, ok := i.Value.(*types.TypeReference) if !ok { continue } @@ -156,8 +198,8 @@ func getArgDescriptors( doc = matches[1] } args = append(args, &api_proto.ArgDescriptor{ - Name: k, - Description: doc + required, + Name: i.Key, + Description: elideDescription(doc) + required, Type: target, }) } @@ -171,7 +213,7 @@ func getArtifactParamDescriptors(artifact *artifacts_proto.Artifact) []*api_prot for _, parameter := range artifact.Parameters { args = append(args, &api_proto.ArgDescriptor{ Name: parameter.Name, - Description: parameter.Description, + Description: elideDescription(parameter.Description), Type: "Artifact Parameter", }) } diff --git a/api/reformat.go b/api/reformat.go new file mode 100644 index 000000000..d58f0ca14 --- /dev/null +++ b/api/reformat.go @@ -0,0 +1,51 @@ +package api + +import ( + "context" + + "www.velocidex.com/golang/velociraptor/acls" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/services" +) + +func (self *ApiServer) ReformatVQL( + ctx context.Context, + in *api_proto.ReformatVQLMessage) (*api_proto.ReformatVQLMessage, error) { + + defer Instrument("ReformatVQL")() + + // Empty creators are called internally. + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, err + } + + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, user_record.Name, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to read notebooks.") + } + + if in.Artifact != "" { + manager, err := services.GetRepositoryManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + reformated_vql, err := manager.ReformatVQL(ctx, in.Artifact) + return &api_proto.ReformatVQLMessage{ + Artifact: reformated_vql, + }, Status(self.verbose, err) + } + notebook_manager, err := services.GetNotebookManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + reformated_vql, err := notebook_manager.ReformatVQL(ctx, in.Vql) + return &api_proto.ReformatVQLMessage{ + Vql: reformated_vql, + }, Status(self.verbose, err) +} diff --git a/api/replication.go b/api/replication.go index aa1e56c7b..15be94476 100644 --- a/api/replication.go +++ b/api/replication.go @@ -1,25 +1,29 @@ package api import ( - "crypto/x509" + "context" "fmt" + "sort" + "strings" + "sync" + "time" "github.com/Velocidex/ordereddict" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/sirupsen/logrus" - context "golang.org/x/net/context" "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" "www.velocidex.com/golang/velociraptor/acls" api_proto "www.velocidex.com/golang/velociraptor/api/proto" + utils "www.velocidex.com/golang/velociraptor/api/utils" config_proto "www.velocidex.com/golang/velociraptor/config/proto" - crypto_utils "www.velocidex.com/golang/velociraptor/crypto/utils" "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/services/debug" + "www.velocidex.com/golang/vfilter" ) var ( @@ -31,6 +35,10 @@ var ( }, []string{"status"}, ) + + gReplicationTracker = &replicationTracker{ + currentReplications: make(map[string]*replicatedStats), + } ) func streamEvents( @@ -38,7 +46,8 @@ func streamEvents( config_obj *config_proto.Config, in *api_proto.EventRequest, stream api_proto.API_WatchEventServer, - peer_name string) (err error) { + peer_name string, + stats *replicatedStats) (err error) { logger := logging.GetLogger(config_obj, &logging.APICmponent) logger.WithFields(logrus.Fields{ @@ -46,7 +55,7 @@ func streamEvents( "user": peer_name, }).Info("Replicating Events") - journal, err := services.GetJournal() + journal, err := services.GetJournal(config_obj) if err != nil { return err } @@ -56,39 +65,62 @@ func streamEvents( if in.Queue == "Server.Internal.MasterRegistrations" { result := ordereddict.NewDict().Set("Events", journal.GetWatchers()) serialized, _ := result.MarshalJSON() - stream.Send(&api_proto.EventResponse{ + err := stream.Send(&api_proto.EventResponse{ Jsonl: serialized, }) + if err != nil { + return err + } + stats.Sent++ } // The API service is running on the master only! This means // the journal service is local. - output_chan, cancel := journal.Watch(ctx, in.Queue) + output_chan, cancel := journal.WatchArtifact( + ctx, in.Queue, "replication-"+in.WatcherName) defer cancel() - for event := range output_chan { - serialized, err := json.Marshal(event) - if err != nil { - continue - } - response := &api_proto.EventResponse{ - Jsonl: serialized, - } - - timer := prometheus.NewTimer( - prometheus.ObserverFunc(func(v float64) { - replicationReceiveHistorgram.WithLabelValues("").Observe(v) - })) - - err = stream.Send(response) - timer.ObserveDuration() - - if err != nil { - continue + for { + select { + case <-ctx.Done(): + return + + case event, ok := <-output_chan: + if !ok { + return + } + + serialized, err := json.Marshal(event) + if err != nil { + continue + } + response := &api_proto.EventResponse{ + Jsonl: serialized, + } + + timer := prometheus.NewTimer( + prometheus.ObserverFunc(func(v float64) { + replicationReceiveHistorgram.WithLabelValues("").Observe(v) + })) + + // If we are not able to send within the sepecified 5 + // seconds we must abort the connection. + + err = utils.DoWithTimeout(func() error { + return stream.Send(response) + }, 5*time.Second) + if err != nil { + return err + } + + timer.ObserveDuration() + stats.Sent++ + + if err != nil { + continue + } } } - - return nil } // NOTE: The API server is only running on the master node. @@ -99,51 +131,119 @@ func (self *ApiServer) WatchEvent( // Get the TLS context from the peer and verify its // certificate. ctx := stream.Context() - peer, ok := peer.FromContext(ctx) - if !ok { - return status.Error(codes.InvalidArgument, "cant get peer info") + users := services.GetUserManager() + user_record, config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return err + } + + // This name is taken from the certificate usually + // VelociraptorServer. + peer_name := user_record.Name + + // Check that the principal is allowed to issue queries. + permissions := acls.ANY_QUERY + ok, err := services.CheckAccess(config_obj, peer_name, permissions) + if err != nil { + return status.Error(codes.PermissionDenied, + fmt.Sprintf("User %v is not allowed to run queries.", + peer_name)) } - tlsInfo, ok := peer.AuthInfo.(credentials.TLSInfo) if !ok { - return status.Error(codes.InvalidArgument, "unable to get credentials") + return status.Error(codes.PermissionDenied, fmt.Sprintf( + "Permission denied: User %v requires permission %v to run queries", + peer_name, permissions)) } - // Authenticate API clients using certificates. - for _, peer_cert := range tlsInfo.State.PeerCertificates { - chains, err := peer_cert.Verify( - x509.VerifyOptions{Roots: self.ca_pool}) - if err != nil { - return err - } + // Update the peer name to make it unique + peer_addr, ok := peer.FromContext(ctx) + if ok { + peer_name = strings.Split(peer_addr.Addr.String(), ":")[0] + } - if len(chains) == 0 { - return status.Error(codes.InvalidArgument, "no chains verified") - } + // Wait here for orderly shutdown of event streams. + self.wg.Add(1) + defer self.wg.Done() + + // The call can access the datastore from any org becuase it is a + // server->server call. + org_manager, err := services.GetOrgManager() + if err != nil { + return err + } - peer_name := crypto_utils.GetSubjectName(peer_cert) + org_config_obj, err := org_manager.GetOrgConfig(in.OrgId) + if err != nil { + return err + } - // Check that the principal is allowed to issue queries. - permissions := acls.ANY_QUERY - ok, err := acls.CheckAccess(self.config, peer_name, permissions) - if err != nil { - return status.Error(codes.PermissionDenied, - fmt.Sprintf("User %v is not allowed to run queries.", - peer_name)) - } + // Cert is good enough for us, run the query. + stats, closer := gReplicationTracker.Add(in.Queue, peer_name, in.OrgId) + defer closer() - if !ok { - return status.Error(codes.PermissionDenied, fmt.Sprintf( - "Permission denied: User %v requires permission %v to run queries", - peer_name, permissions)) - } + return streamEvents( + ctx, org_config_obj, in, stream, peer_name, stats) +} - // return the first good match - if true { - // Cert is good enough for us, run the query. - return streamEvents(ctx, self.config, in, stream, peer_name) - } +type replicatedStats struct { + Sent int +} + +type replicationTracker struct { + mu sync.Mutex + currentReplications map[string]*replicatedStats +} + +func (self *replicationTracker) Debug() []*ordereddict.Dict { + self.mu.Lock() + defer self.mu.Unlock() + + result := []*ordereddict.Dict{} + keys := []string{} + for k := range self.currentReplications { + keys = append(keys, k) + } + + sort.Strings(keys) + for _, k := range keys { + v, _ := self.currentReplications[k] + result = append(result, ordereddict.NewDict(). + Set("Type", "Replication"). + Set("Name", k). + Set("Stats", v)) } + return result +} + +func (self *replicationTracker) Add(queue, peer, org_id string) (*replicatedStats, func()) { + key := queue + "->" + peer + " " + org_id + self.mu.Lock() + defer self.mu.Unlock() + + stats := &replicatedStats{} - return status.Error(codes.InvalidArgument, "no peer certs?") + self.currentReplications[key] = stats + + return stats, func() { + self.mu.Lock() + defer self.mu.Unlock() + + delete(self.currentReplications, key) + } +} + +func init() { + debug.RegisterProfileWriter(debug.ProfileWriterInfo{ + Name: "Replication", + Description: "Report current replication connections between master and minion", + Categories: []string{"Global", "Datastore"}, + ProfileWriter: func(ctx context.Context, + scope vfilter.Scope, output_chan chan vfilter.Row) { + + for _, i := range gReplicationTracker.Debug() { + output_chan <- i + } + }, + }) } diff --git a/api/reports.go b/api/reports.go index a684f24cf..0ca2413b0 100644 --- a/api/reports.go +++ b/api/reports.go @@ -1,20 +1,30 @@ package api import ( + "context" "fmt" "strings" - errors "github.com/pkg/errors" - context "golang.org/x/net/context" + "github.com/Velocidex/ordereddict" + errors "github.com/go-errors/errors" api_proto "www.velocidex.com/golang/velociraptor/api/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/paths" "www.velocidex.com/golang/velociraptor/reporting" "www.velocidex.com/golang/velociraptor/services" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" ) +// Reports are used for various dashboards. They are almost like a +// notebook (but historically predate it). +// TODO: Think about consolidating reports and notebooks +// Currently this is used from: +// 1. Home screen (dashboard) (type: "SERVER_EVENT") +// 2. Client's VQL Drilldown screen (type: "CLIENT") +// 3. View Artifacts screen (type: "ARTIFACT_DESCRIPTION") func getReport(ctx context.Context, config_obj *config_proto.Config, acl_manager vql_subsystem.ACLManager, @@ -22,17 +32,48 @@ func getReport(ctx context.Context, in *api_proto.GetReportRequest) ( *api_proto.GetReportResponse, error) { + // Dashboards receive their own notebook ID in a predictable + // location. + bare_artifact_name := strings.TrimPrefix(in.Artifact, + constants.ARTIFACT_CUSTOM_NAME_PREFIX) + + notebook_cell_path_manager := paths.NewDashboardPathManager( + in.Type, bare_artifact_name, in.ClientId) + + uploader := reporting.NewNotebookUploader( + config_obj, notebook_cell_path_manager) + + builder := services.ScopeBuilder{ + Config: config_obj, + ACLManager: acl_manager, + Uploader: uploader, + Logger: logging.NewPlainLogger( + config_obj, &logging.FrontendComponent), + Repository: repository, + Env: ordereddict.NewDict(), + } + + manager, err := services.GetRepositoryManager(config_obj) + if err != nil { + return nil, err + } + + scope := manager.BuildScope(builder) + defer scope.Close() + template_engine, err := reporting.NewGuiTemplateEngine( - config_obj, ctx, nil, /* default scope */ - acl_manager, repository, nil, in.Artifact) + config_obj, ctx, scope, + acl_manager, repository, + notebook_cell_path_manager, + in.Artifact) if err != nil { if strings.HasPrefix(in.Artifact, constants.ARTIFACT_CUSTOM_NAME_PREFIX) { template_engine, err = reporting.NewGuiTemplateEngine( - config_obj, ctx, nil, /* default scope */ - acl_manager, repository, nil, - strings.TrimPrefix(in.Artifact, - constants.ARTIFACT_CUSTOM_NAME_PREFIX)) + config_obj, ctx, scope, + acl_manager, repository, + notebook_cell_path_manager, + bare_artifact_name) } if err != nil { return nil, err @@ -43,9 +84,10 @@ func getReport(ctx context.Context, var template_data string if in.Type == "" { - definition, pres := repository.Get(config_obj, "Custom."+in.Artifact) + definition, pres := repository.Get( + ctx, config_obj, "Custom."+in.Artifact) if !pres { - definition, pres = repository.Get(config_obj, in.Artifact) + definition, pres = repository.Get(ctx, config_obj, in.Artifact) } if pres { for _, report := range definition.Reports { @@ -87,7 +129,7 @@ func getReport(ctx context.Context, template_engine, in.ClientId, in.StartTime, in.EndTime) case "ARTIFACT_DESCRIPTION": - template_data, err = reporting.GenerateArtifactDescriptionReport( + template_data, err = reporting.GenerateArtifactDescriptionReport(ctx, template_engine, config_obj) } diff --git a/api/scheduler.go b/api/scheduler.go new file mode 100644 index 000000000..6517d77cf --- /dev/null +++ b/api/scheduler.go @@ -0,0 +1,140 @@ +package api + +import ( + "fmt" + "io" + "strings" + "sync" + + errors "github.com/go-errors/errors" + "google.golang.org/grpc/peer" + "www.velocidex.com/golang/velociraptor/acls" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" +) + +func (self *ApiServer) Scheduler( + stream api_proto.API_SchedulerServer) error { + + defer Instrument("Scheduler")() + + users := services.GetUserManager() + ctx := stream.Context() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return Status(self.verbose, err) + } + + // This is usually only for minions + permissions := acls.DATASTORE_ACCESS + peer_name := user_record.Name + perm, err := services.CheckAccess(org_config_obj, peer_name, permissions) + if !perm || err != nil { + return PermissionDenied(err, + fmt.Sprintf("User %v is not allowed to read notebooks.", peer_name)) + } + + // Update the peer name to make it unique + peer_addr, ok := peer.FromContext(ctx) + if ok { + peer_name = strings.Split(peer_addr.Addr.String(), ":")[0] + } + + scheduler, err := services.GetSchedulerService(org_config_obj) + if err != nil { + return Status(self.verbose, err) + } + + req, err := stream.Recv() + if err == io.EOF { + return nil + } + + if err != nil { + return Status(self.verbose, err) + } + + if req.Queue == "" || req.Type != "register" { + return errors.New("First request must be a register request") + } + + job_chan, err := scheduler.RegisterWorker(ctx, req.Queue, + peer_name, int(req.Priority)) + if err != nil { + return Status(self.verbose, err) + } + + var mu sync.Mutex + in_flight := make(map[uint64]services.SchedulerJob) + defer func() { + mu.Lock() + defer mu.Unlock() + + // There should be no jobs in flight unless the client + // suddenly disconnects + for _, j := range in_flight { + j.Done("", errors.New("Disconnected")) + } + }() + + // Watch for responses and close off any outstanding ones. + go func() { + for { + req, err := stream.Recv() + if err == io.EOF { + return + } + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.FrontendComponent) + logger.Error("Scheduler: %v", err) + return + } + + if req.Type == "response" { + mu.Lock() + in_flight_job, pres := in_flight[req.Id] + if pres { + var err error + if req.Error != "" { + err = errors.New(req.Error) + } + in_flight_job.Done(req.Response, err) + delete(in_flight, req.Id) + } + mu.Unlock() + } + + } + }() + + // Spin forever waiting on jobs + for { + select { + case <-ctx.Done(): + return nil + + case job_req, ok := <-job_chan: + if !ok { + return nil + } + + // Hold onto the job until response comes. + id := utils.GetId() + mu.Lock() + in_flight[id] = job_req + mu.Unlock() + + err := stream.Send(&api_proto.ScheduleResponse{ + Id: id, + Queue: job_req.Queue, + Job: job_req.Job, + OrgId: job_req.OrgId, + }) + if err != nil { + return Status(self.verbose, err) + } + } + } +} diff --git a/api/secrets.go b/api/secrets.go new file mode 100644 index 000000000..3958ce03c --- /dev/null +++ b/api/secrets.go @@ -0,0 +1,142 @@ +package api + +import ( + "context" + + "github.com/Velocidex/ordereddict" + "google.golang.org/protobuf/types/known/emptypb" + "www.velocidex.com/golang/velociraptor/acls" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/services" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" +) + +func (self *ApiServer) GetSecretDefinitions( + ctx context.Context, + in *emptypb.Empty) (*api_proto.SecretDefinitionList, error) { + + defer Instrument("GetSecretDefinitions")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to view secrets.") + } + + secrets, err := services.GetSecretsService(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + return &api_proto.SecretDefinitionList{ + Items: secrets.GetSecretDefinitions(ctx), + }, nil + +} + +func (self *ApiServer) AddSecret( + ctx context.Context, + in *api_proto.Secret) (*emptypb.Empty, error) { + + defer Instrument("AddSecret")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + permissions := acls.SERVER_ADMIN + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to manage secrets.") + } + + secrets, err := services.GetSecretsService(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + secret_data := ordereddict.NewDict() + for k, v := range in.Secret { + secret_data.Set(k, v) + } + + scope := vql_subsystem.MakeScope() + err = secrets.AddSecret(ctx, scope, in.TypeName, in.Name, secret_data) + return &emptypb.Empty{}, Status(self.verbose, err) +} + +func (self *ApiServer) GetSecret( + ctx context.Context, + in *api_proto.Secret) (*api_proto.Secret, error) { + + defer Instrument("GetSecret")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + permissions := acls.SERVER_ADMIN + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to manage secrets.") + } + + secrets, err := services.GetSecretsService(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + secret, err := secrets.GetSecretMetadata(ctx, in.TypeName, in.Name) + if err != nil { + return nil, Status(self.verbose, err) + } + + // Return a redacted version of the secret so the GUI can render + // the secret metadata + return secret.Secret, nil +} + +func (self *ApiServer) ModifySecret( + ctx context.Context, + in *api_proto.ModifySecretRequest) (*emptypb.Empty, error) { + + defer Instrument("ModifySecret")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + permissions := acls.SERVER_ADMIN + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to manage secrets.") + } + + secrets, err := services.GetSecretsService(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + err = secrets.ModifySecret(ctx, in) + return &emptypb.Empty{}, Status(self.verbose, err) +} diff --git a/api/server_monitoring.go b/api/server_monitoring.go deleted file mode 100644 index bdc95167a..000000000 --- a/api/server_monitoring.go +++ /dev/null @@ -1,45 +0,0 @@ -package api - -import ( - config_proto "www.velocidex.com/golang/velociraptor/config/proto" - "www.velocidex.com/golang/velociraptor/datastore" - flows_proto "www.velocidex.com/golang/velociraptor/flows/proto" - "www.velocidex.com/golang/velociraptor/paths" - "www.velocidex.com/golang/velociraptor/services" -) - -func getServerMonitoringState(config_obj *config_proto.Config) ( - *flows_proto.ArtifactCollectorArgs, error) { - db, err := datastore.GetDB(config_obj) - if err != nil { - return nil, err - } - - result := &flows_proto.ArtifactCollectorArgs{} - err = db.GetSubject(config_obj, - paths.ServerMonitoringFlowURN, - result, - ) - _ = err // if an error we return an empty collector args. - - return result, nil -} - -func setServerMonitoringState( - config_obj *config_proto.Config, - principal string, - args *flows_proto.ArtifactCollectorArgs) error { - db, err := datastore.GetDB(config_obj) - if err != nil { - return err - } - - err = services.GetServerEventManager().Update(config_obj, principal, args) - if err != nil { - return err - } - - return db.SetSubject( - config_obj, paths.ServerMonitoringFlowURN, - args) -} diff --git a/api/static.go b/api/static.go new file mode 100644 index 000000000..bba270813 --- /dev/null +++ b/api/static.go @@ -0,0 +1,164 @@ +// Implement transparent asset decompression and caching. + +// Most modern browsers can negotiate compression transfer with no +// issues. In this case we just deliver the compressed assets directly +// saving on both bandwidth and CPU cycles. + +// However older browsers may not support brotli compression, so we +// need to decompress the asset for them and cache it for a short +// time. + +package api + +import ( + "bytes" + "context" + "io" + "io/fs" + "net/http" + "os" + "strings" + "time" + + "github.com/Velocidex/ttlcache/v2" + "github.com/andybalholm/brotli" + errors "github.com/go-errors/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "www.velocidex.com/golang/velociraptor/services" +) + +var ( + brotliDecompressionCounter = promauto.NewCounter(prometheus.CounterOpts{ + Name: "gui_asset_decompression_count", + Help: "Number of times the GUI was forced to decompressed assets for browsers that do not support brotli compression.", + }) +) + +type memFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (f *memFileInfo) Name() string { return f.name } +func (f *memFileInfo) Size() int64 { return f.size } +func (f *memFileInfo) Mode() os.FileMode { return f.mode } +func (f *memFileInfo) ModTime() time.Time { return f.modTime } +func (f *memFileInfo) IsDir() bool { return f.mode.IsDir() } +func (f *memFileInfo) Sys() interface{} { return nil } + +type brotliBuffer struct { + *bytes.Reader + name string + size int64 +} + +func (self *brotliBuffer) Close() error { + return nil +} + +func (self *brotliBuffer) Readdir(count int) ([]fs.FileInfo, error) { + return nil, errors.New("Not Implemented") +} + +func (self *brotliBuffer) Stat() (fs.FileInfo, error) { + return &memFileInfo{ + name: self.name, + size: self.size, + mode: 0644, + }, nil +} + +type CachedFilesystem struct { + http.FileSystem + lru *ttlcache.Cache +} + +func (self *CachedFilesystem) getCachedBytes(name string) ([]byte, error) { + cached_any, err := self.lru.Get(name) + if err != nil { + return nil, err + } + + cached, ok := cached_any.([]byte) + if !ok { + return nil, errors.New("Invalid cached item") + } + return cached, nil +} + +func (self *CachedFilesystem) Open(name string) (http.File, error) { + // We do not support gz files at all - it is either brotli or + // uncompressed. + if strings.HasSuffix(name, ".gz") { + return nil, services.OrgNotFoundError + } + + fd, err := self.FileSystem.Open(name) + if err != nil { + // If there is not brotli file, it is just not there. + if strings.HasSuffix(name, ".br") { + return nil, services.OrgNotFoundError + } + + // Check if a compressed .br file exists + fd, err := self.FileSystem.Open(name + ".br") + if err == nil { + cached, err := self.getCachedBytes(name) + if err == nil { + return &brotliBuffer{ + Reader: bytes.NewReader(cached), + name: name, + size: int64(len(cached)), + }, nil + } + + out_fd := &bytes.Buffer{} + n, err := io.Copy(out_fd, brotli.NewReader(fd)) + if err != nil { + return nil, err + } + + brotliDecompressionCounter.Inc() + + // Cache for next time. + err = self.lru.Set(name, out_fd.Bytes()) + + return &brotliBuffer{ + Reader: bytes.NewReader(out_fd.Bytes()), + name: name, + size: n, + }, err + } + } + return fd, err +} + +func (self *CachedFilesystem) Exists(path string) bool { + fd, err := self.FileSystem.Open(path) + if err != nil { + return false + } + fd.Close() + return true +} + +func NewCachedFilesystem( + ctx context.Context, fs http.FileSystem) *CachedFilesystem { + result := &CachedFilesystem{ + FileSystem: fs, + lru: ttlcache.NewCache(), + } + + _ = result.lru.SetTTL(10 * time.Minute) + result.lru.SkipTTLExtensionOnHit(true) + + go func() { + <-ctx.Done() + result.lru.Close() + }() + + return result +} diff --git a/api/status.go b/api/status.go new file mode 100644 index 000000000..6ff5d454a --- /dev/null +++ b/api/status.go @@ -0,0 +1,66 @@ +package api + +import ( + "fmt" + "os" + + errors "github.com/go-errors/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/utils" +) + +// Convert from various errors into gRPC status errors. This will be +// translated into proper HTTP codes by the gRPC gateway +func Status(verbose bool, err error) error { + // Do not interfer with status messages already. + _, ok := status.FromError(err) + if ok { + return err + } + + // Do not report this error. + if err == utils.InlineError { + return nil + } + + if errors.Is(err, acls.PermissionDenied) { + return status.Error(codes.PermissionDenied, err.Error()) + } + + if errors.Is(err, utils.InvalidStatus) { + return status.Error(codes.InvalidArgument, err.Error()) + } + + // With the verbose flag give more detailed errors to the browser. + if verbose { + if errors.Is(err, os.ErrNotExist) { + return status.Error(codes.NotFound, err.Error()) + } + + return err + } + + // In production provide generic errors. + if errors.Is(err, os.ErrNotExist) { + return status.Error(codes.NotFound, "Not Found") + } + + // TODO: For now unknown errors will be returned to the user, but + // we need to tighten it here to prevent internal information + // leak. + return status.Error(codes.Unavailable, err.Error()) +} + +func InvalidStatus(message string) error { + return fmt.Errorf("%w: %s", utils.InvalidStatus, message) +} + +func PermissionDenied(err error, message string) error { + if err != nil { + return status.Error(codes.PermissionDenied, + fmt.Sprintf("%v: %v", err, message)) + } + return status.Error(codes.PermissionDenied, message) +} diff --git a/api/tables/doc.go b/api/tables/doc.go new file mode 100644 index 000000000..b875d6ac4 --- /dev/null +++ b/api/tables/doc.go @@ -0,0 +1,10 @@ +package tables + +// The GUI frequently needs to display tables. This package manages +// the GUI's table access: + +// 1. Central place for managing where result sets are stored and +// accessed for the different tables needed + +// 2. Apply any result set transformations needed on the underlying +// result set implementation. diff --git a/api/tables/notebooks.go b/api/tables/notebooks.go new file mode 100644 index 000000000..92af60020 --- /dev/null +++ b/api/tables/notebooks.go @@ -0,0 +1,28 @@ +package tables + +import ( + "context" + + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/services" +) + +func getNotebookTable( + ctx context.Context, + config_obj *config_proto.Config, + in *api_proto.GetTableRequest, + principal string) (*api_proto.GetTableResponse, error) { + + notebook_manager, err := services.GetNotebookManager(config_obj) + if err != nil { + return nil, err + } + + _, err = notebook_manager.GetSharedNotebooks(ctx, principal) + if err != nil { + return nil, err + } + + return getTable(ctx, config_obj, in, principal) +} diff --git a/api/tables/table.go b/api/tables/table.go new file mode 100644 index 000000000..3581ebd86 --- /dev/null +++ b/api/tables/table.go @@ -0,0 +1,488 @@ +/* +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ +package tables + +import ( + "context" + "io" + "os" + "regexp" + "strings" + "time" + + "github.com/Velocidex/ordereddict" + errors "github.com/go-errors/errors" + "www.velocidex.com/golang/velociraptor/constants" + file_store "www.velocidex.com/golang/velociraptor/file_store" + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/file_store/path_specs" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/paths" + "www.velocidex.com/golang/velociraptor/paths/artifact_modes" + "www.velocidex.com/golang/velociraptor/paths/artifacts" + "www.velocidex.com/golang/velociraptor/result_sets" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" + + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" +) + +func GetTable( + ctx context.Context, + config_obj *config_proto.Config, + in *api_proto.GetTableRequest, + principal string) ( + *api_proto.GetTableResponse, error) { + + var result *api_proto.GetTableResponse + var err error + + // We want an event table. + switch in.Type { + case "TIMELINE": + result, err = getTimeline(ctx, config_obj, in) + + case "CLIENT_EVENT_LOGS", "SERVER_EVENT_LOGS": + result, err = getEventTableLogs(ctx, config_obj, in) + + case "CLIENT_EVENT", "SERVER_EVENT": + result, err = getEventTable(ctx, config_obj, in) + + case "STACK": + result, err = getStackTable(ctx, config_obj, in) + + case "NOTEBOOKS": + result, err = getNotebookTable(ctx, config_obj, in, principal) + + default: + result, err = getTable(ctx, config_obj, in, principal) + } + + if err != nil { + return nil, err + } + + if in.Artifact != "" { + manager, err := services.GetRepositoryManager(config_obj) + if err != nil { + return nil, err + } + + repository, err := manager.GetGlobalRepository(config_obj) + if err != nil { + return nil, err + } + + artifact, pres := repository.Get(ctx, config_obj, in.Artifact) + if pres { + result.ColumnTypes = artifact.ColumnTypes + } + } + + return result, nil + +} + +func getTable( + ctx context.Context, + config_obj *config_proto.Config, + in *api_proto.GetTableRequest, + principal string) ( + *api_proto.GetTableResponse, error) { + + if in.Rows == 0 { + in.Rows = 2000 + } + + result := &api_proto.GetTableResponse{ + ColumnTypes: getColumnTypes(ctx, config_obj, in), + } + + path_spec, err := GetPathSpec(ctx, config_obj, in, principal) + if err != nil { + return result, err + } + + file_store_factory := file_store.GetFileStore(config_obj) + + options, err := GetTableOptions(in) + if err != nil { + return result, err + } + + rs_reader, err := result_sets.NewResultSetReaderWithOptions( + ctx, config_obj, + file_store_factory, path_spec, options) + + // if the result does not exist yet, just return an empty result. + if errors.Is(err, os.ErrNotExist) { + return result, nil + } + + if err != nil { + return nil, err + } + defer rs_reader.Close() + + // Let the browser know how many rows we have in total. + result.TotalRows = rs_reader.TotalRows() + + // FIXME: Backwards compatibility: Just give a few + // rows if the result set does not have an index. This + // is the same as the previous behavior but for new + // collections, an index is created and we respect the + // number of rows the callers asked for. Eventually + // this will not be needed. + if result.TotalRows < 0 { + in.Rows = 100 + } + + stack_path := rs_reader.Stacker() + if !utils.IsNil(stack_path) { + result.StackPath = stack_path.Components() + } + + // Seek to the row we need. + err = rs_reader.SeekToRow(int64(in.StartRow)) + if errors.Is(err, io.EOF) { + return result, nil + } + + if err != nil { + return nil, err + } + + return ConvertRowsToTableResponse( + rs_reader.Rows(ctx), result, in.Timezone, in.Rows), nil +} + +func getStackTable( + ctx context.Context, + config_obj *config_proto.Config, + in *api_proto.GetTableRequest) ( + *api_proto.GetTableResponse, error) { + + if in.Rows == 0 { + in.Rows = 2000 + } + + result := &api_proto.GetTableResponse{ + ColumnTypes: getColumnTypes(ctx, config_obj, in), + } + + path_spec := path_specs.NewUnsafeFilestorePath( + utils.FilterSlice(in.StackPath, "")...). + SetType(api.PATH_TYPE_FILESTORE_JSON) + file_store_factory := file_store.GetFileStore(config_obj) + + options, err := GetTableOptions(in) + if err != nil { + return result, err + } + + rs_reader, err := result_sets.NewResultSetReaderWithOptions( + ctx, config_obj, + file_store_factory, path_spec, options) + + if err != nil { + return result, nil + } + defer rs_reader.Close() + + // Let the browser know how many rows we have in total. + result.TotalRows = rs_reader.TotalRows() + + // FIXME: Backwards compatibility: Just give a few + // rows if the result set does not have an index. This + // is the same as the previous behavior but for new + // collections, an index is created and we respect the + // number of rows the callers asked for. Eventually + // this will not be needed. + if result.TotalRows < 0 { + in.Rows = 100 + } + + // Seek to the row we need. + err = rs_reader.SeekToRow(int64(in.StartRow)) + if errors.Is(err, io.EOF) { + return result, nil + } + + if err != nil { + return nil, err + } + + return ConvertRowsToTableResponse( + rs_reader.Rows(ctx), result, in.Timezone, in.Rows), nil +} + +// The GUI is requesting table data. This function tries to figure out +// the column types. +func getColumnTypes( + ctx context.Context, config_obj *config_proto.Config, + in *api_proto.GetTableRequest) []*artifacts_proto.ColumnType { + + // For artifacts column types are specified in the `column_types` + // artifact definition. + if in.Artifact != "" { + manager, err := services.GetRepositoryManager(config_obj) + if err != nil { + return nil + } + + repository, err := manager.GetGlobalRepository(config_obj) + if err != nil { + return nil + } + + artifact, pres := repository.Get(ctx, config_obj, in.Artifact) + if pres { + return artifact.ColumnTypes + } + } + + // For notebooks, the column_types are set in the notebook metadata. + if in.NotebookId != "" { + notebook_manager, err := services.GetNotebookManager(config_obj) + if err != nil { + return nil + } + + notebook, err := notebook_manager.GetNotebook(ctx, in.NotebookId, + services.DO_NOT_INCLUDE_UPLOADS) + if err != nil { + return nil + } + return notebook.ColumnTypes + } + + return nil +} + +// Get the relevant pathspec for the table needed. Basically a big +// switch to figure out where the result set we want to look at is +// stored. +func GetPathSpec( + ctx context.Context, config_obj *config_proto.Config, + in *api_proto.GetTableRequest, principal string) (api.FSPathSpec, error) { + + if in.Type == "CLIENT_FLOWS" && in.ClientId != "" { + return paths.NewClientPathManager(in.ClientId).FlowIndex(), nil + } + + if in.Type == "NOTEBOOKS" { + return paths.NewNotebookPathManager(""). + NotebookIndexForUser(principal), nil + } + + if in.Type == "USER_MESSAGES" { + return paths.NewUserPathManager(principal).Notifications(), nil + } + + if in.FlowId != "" && in.Artifact != "" { + mode := artifact_modes.MODE_CLIENT + if in.ClientId == constants.VELOCIRAPTOR_SERVER_CLIENT_ID { + mode = artifact_modes.MODE_SERVER + } + return artifacts.NewArtifactPathManagerWithMode( + config_obj, in.ClientId, in.FlowId, in.Artifact, + mode).Path(), nil + + } else if in.FlowId != "" && in.Type != "" { + flow_path_manager := paths.NewFlowPathManager( + in.ClientId, in.FlowId) + + switch in.Type { + case "log": + return flow_path_manager.Log(), nil + + case "uploads": + return flow_path_manager.UploadMetadata(), nil + + case "upload_transactions": + return flow_path_manager.UploadTransactions(), nil + } + + } else if in.HuntId != "" && in.Type == "clients" { + return paths.NewHuntPathManager(in.HuntId).Clients(), nil + + } else if in.HuntId != "" && in.Type == "hunt_status" { + return paths.NewHuntPathManager(in.HuntId).ClientErrors(), nil + + } else if in.NotebookId != "" && in.CellId != "" && in.Type == "logs" { + return paths.NewNotebookPathManager(in.NotebookId).Cell( + in.CellId, in.CellVersion).Logs(), nil + + // Handle dashboards specially. Dashboards are kind of + // non-interactive notebook stored in a special notebook ID + // called "Dashboards". Cells within the dashboard correspond + // to different artifacts. Dashboard cells are recalculated + // each time they are viewed. + } else if in.NotebookId == "Dashboards" && in.CellId != "" { + return paths.NewDashboardPathManager(in.Type, in.CellId, in.ClientId). + QueryStorage(in.TableId).Path(), nil + + } else if in.NotebookId != "" && in.CellId != "" { + return paths.NewNotebookPathManager(in.NotebookId).Cell( + in.CellId, in.CellVersion).QueryStorage(in.TableId).Path(), nil + } + + return nil, errors.New("Invalid request") +} + +func getEventTable( + ctx context.Context, + config_obj *config_proto.Config, + in *api_proto.GetTableRequest) ( + *api_proto.GetTableResponse, error) { + path_manager, err := artifacts.NewArtifactPathManager(ctx, + config_obj, in.ClientId, in.FlowId, in.Artifact) + if err != nil { + return nil, err + } + + return getEventTableWithPathManager(ctx, config_obj, in, path_manager) +} + +func getEventTableLogs( + ctx context.Context, + config_obj *config_proto.Config, + in *api_proto.GetTableRequest) ( + *api_proto.GetTableResponse, error) { + path_manager, err := artifacts.NewArtifactLogPathManager(ctx, + config_obj, in.ClientId, "", in.Artifact) + if err != nil { + return nil, err + } + return getEventTableWithPathManager(ctx, config_obj, in, path_manager) +} + +// Unpack the rows into the output protobuf. Although not ideal, each +// row can have a different set of columns that the previous row. We +// keep track of all columns seen in this table page and their +// relative order. +func ConvertRowsToTableResponse( + in <-chan *ordereddict.Dict, + result *api_proto.GetTableResponse, + timezone string, + limit uint64, +) *api_proto.GetTableResponse { + opts := json.GetJsonOptsForTimezone(timezone) + + var rows uint64 + column_known := make(map[string]bool) + for row := range in { + data := make(map[string]interface{}) + for _, i := range row.Items() { + // Do we already know about this column? + _, pres := column_known[i.Key] + if !pres { + result.Columns = append(result.Columns, i.Key) + column_known[i.Key] = true + } + + data[i.Key] = i.Value + } + + json_out := make([]interface{}, 0, len(result.Columns)) + for _, k := range result.Columns { + value, _ := data[k] + json_out = append(json_out, value) + } + serialized, err := json.MarshalWithOptions(json_out, opts) + if err != nil { + continue + } + + result.Rows = append(result.Rows, &api_proto.Row{ + Json: string(serialized), + }) + + rows += 1 + if rows >= limit { + break + } + } + + return result +} + +func getEventTableWithPathManager( + ctx context.Context, + config_obj *config_proto.Config, + in *api_proto.GetTableRequest, + path_manager api.PathManager) ( + *api_proto.GetTableResponse, error) { + + if in.Rows == 0 { + in.Rows = 10 + } + + result := &api_proto.GetTableResponse{} + + rs_reader, err := result_sets.NewTimedResultSetReader(ctx, + config_obj, path_manager) + if err != nil { + return nil, err + } + defer rs_reader.Close() + + err = rs_reader.SeekToTime(time.Unix(int64(in.StartTime), 0)) + if err != nil { + return nil, err + } + + if in.EndTime != 0 { + rs_reader.SetMaxTime(time.Unix(int64(in.EndTime), 0)) + } + + return ConvertRowsToTableResponse( + rs_reader.Rows(ctx), result, in.Timezone, in.Rows), nil +} + +func GetTableOptions(in *api_proto.GetTableRequest) ( + options result_sets.ResultSetOptions, err error) { + if in.SortColumn != "" { + options.SortColumn = in.SortColumn + options.SortAsc = in.SortDirection + } + + if in.FilterColumn != "" && + in.FilterRegex != "" { + options.FilterColumn = in.FilterColumn + + // If the filter has a ! in the first position it excludes the + // match. + if strings.HasPrefix(in.FilterRegex, "!") { + in.FilterRegex = in.FilterRegex[1:] + options.FilterExclude = true + } + + options.FilterRegex, err = regexp.Compile("(?i)" + in.FilterRegex) + if err != nil { + return options, err + } + } + + options.StartIdx = in.StartIdx + options.EndIdx = in.EndIdx + + return options, nil +} diff --git a/api/tables/timelines.go b/api/tables/timelines.go new file mode 100644 index 000000000..9733906f3 --- /dev/null +++ b/api/tables/timelines.go @@ -0,0 +1,114 @@ +package tables + +import ( + "context" + "time" + + errors "github.com/go-errors/errors" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/services" +) + +func getTimeline( + ctx context.Context, + config_obj *config_proto.Config, + in *api_proto.GetTableRequest) (*api_proto.GetTableResponse, error) { + + if in.NotebookId == "" { + return nil, errors.New("NotebookId must be specified") + } + + notebook_manager, err := services.GetNotebookManager(config_obj) + if err != nil { + return nil, err + } + + options := services.TimelineOptions{ + IncludeComponents: in.IncludeComponents, + ExcludeComponents: in.SkipComponents} + + if in.StartTime != 0 { + options.StartTime = time.Unix(0, int64(in.StartTime)) + } + + if in.FilterRegex != "" { + options.Filter = in.FilterRegex + } + + reader, err := notebook_manager.ReadTimeline(ctx, in.NotebookId, + in.Timeline, options) + if err != nil { + return nil, err + } + + result := &api_proto.GetTableResponse{ + Timelines: reader.Stat().Timelines, + } + return ConvertTimelineRowsToTableResponse( + ctx, reader, result, in.Timezone, in.Rows), nil +} + +func ConvertTimelineRowsToTableResponse( + ctx context.Context, + reader services.TimelineReader, + result *api_proto.GetTableResponse, + timezone string, + limit uint64, +) *api_proto.GetTableResponse { + opts := json.GetJsonOptsForTimezone(timezone) + + var rows uint64 + column_known := make(map[string]bool) + for row := range reader.Read(ctx) { + // Row has timestamp + timestamp_any, pres := row.Get("Timestamp") + if !pres { + continue + } + + timestamp, ok := timestamp_any.(time.Time) + if !ok { + continue + } + + if result.StartTime == 0 { + result.StartTime = timestamp.UnixNano() + } + result.EndTime = timestamp.UnixNano() + + data := make(map[string]interface{}) + for _, i := range row.Items() { + // Do we already know about this column? + _, pres := column_known[i.Key] + if !pres { + result.Columns = append(result.Columns, i.Key) + column_known[i.Key] = true + } + + data[i.Key] = i.Value + } + + json_out := make([]interface{}, 0, len(result.Columns)) + for _, k := range result.Columns { + value, _ := data[k] + json_out = append(json_out, value) + } + serialized, err := json.MarshalWithOptions(json_out, opts) + if err != nil { + continue + } + + result.Rows = append(result.Rows, &api_proto.Row{ + Json: string(serialized), + }) + + rows += 1 + if rows >= limit { + break + } + } + + return result +} diff --git a/api/timelines.go b/api/timelines.go new file mode 100644 index 000000000..42f02380b --- /dev/null +++ b/api/timelines.go @@ -0,0 +1,53 @@ +package api + +import ( + "context" + "time" + + "github.com/Velocidex/ordereddict" + "google.golang.org/protobuf/types/known/emptypb" + "www.velocidex.com/golang/velociraptor/acls" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/json" + "www.velocidex.com/golang/velociraptor/services" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" +) + +func (self *ApiServer) AnnotateTimeline( + ctx context.Context, + in *api_proto.AnnotationRequest) (*emptypb.Empty, error) { + + defer Instrument("AnnotateTimeline")() + + // Empty creators are called internally. + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, err + } + principal := user_record.Name + + permissions := acls.NOTEBOOK_EDITOR + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, "User is not allowed to update notebooks.") + } + + notebook_manager, err := services.GetNotebookManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + scope := vql_subsystem.MakeScope() + event := ordereddict.NewDict() + err = json.Unmarshal([]byte(in.EventJson), &event) + if err != nil { + return nil, Status(self.verbose, err) + } + + err = notebook_manager.AnnotateTimeline(ctx, scope, in.NotebookId, + in.SuperTimeline, in.Note, principal, time.Unix(0, in.Timestamp), + event) + + return &emptypb.Empty{}, Status(self.verbose, err) +} diff --git a/api/tools.go b/api/tools.go index 8e155407f..41adabb1d 100644 --- a/api/tools.go +++ b/api/tools.go @@ -1,71 +1,90 @@ package api import ( - context "golang.org/x/net/context" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "context" "www.velocidex.com/golang/velociraptor/acls" artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" "www.velocidex.com/golang/velociraptor/services" - users "www.velocidex.com/golang/velociraptor/users" ) func (self *ApiServer) GetToolInfo(ctx context.Context, in *artifacts_proto.Tool) (*artifacts_proto.Tool, error) { - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + + defer Instrument("GetToolInfo")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, user_record.Name, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to view tools.") } + inventory, err := services.GetInventory(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } if in.Materialize { - return services.GetInventory().GetToolInfo(ctx, self.config, in.Name) + return inventory.GetToolInfo(ctx, org_config_obj, in.Name, in.Version) } - return services.GetInventory().ProbeToolInfo(in.Name) + tool, err := inventory.ProbeToolInfo(ctx, org_config_obj, in.Name, in.Version) + return tool, Status(self.verbose, err) } func (self *ApiServer) SetToolInfo(ctx context.Context, in *artifacts_proto.Tool) (*artifacts_proto.Tool, error) { - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + + defer Instrument("SetToolInfo")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } // Minimum permission required. If the user can write // artifacts they can already autoload tools by uploading an // artifact definition. permissions := acls.ARTIFACT_WRITER - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) + perm, err := services.CheckAccess(org_config_obj, user_record.Name, permissions) if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, + return nil, PermissionDenied(err, "User is not allowed to update tool definitions.") } materialize := in.Materialize in.Materialize = false - err = services.GetInventory().AddTool(self.config, in, + + inventory, err := services.GetInventory(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + // Clear internally managed fields the user should not be allowed + // to set. + in.Versions = nil + in.ServeUrl = "" + in.InvalidHash = "" + + err = inventory.AddTool(ctx, org_config_obj, in, services.ToolOptions{ AdminOverride: true, }) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } // If materialized we re-fetch the tool and send back the full // record. if materialize { - return services.GetInventory().GetToolInfo(ctx, self.config, - in.Name) + return inventory.GetToolInfo(ctx, org_config_obj, in.Name, in.Version) } return in, nil diff --git a/api/upload.go b/api/upload.go index 0c509fd7d..9f29d71b6 100644 --- a/api/upload.go +++ b/api/upload.go @@ -9,200 +9,253 @@ import ( "path" "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/api/authenticators" api_proto "www.velocidex.com/golang/velociraptor/api/proto" + api_utils "www.velocidex.com/golang/velociraptor/api/utils" artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" - file_store "www.velocidex.com/golang/velociraptor/file_store" "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/paths" "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" ) -func toolUploadHandler( - config_obj *config_proto.Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check for acls - userinfo := GetUserInfo(r.Context(), config_obj) - permissions := acls.ARTIFACT_WRITER - perm, err := acls.CheckAccess(config_obj, userinfo.Name, permissions) - if !perm || err != nil { - returnError(w, http.StatusUnauthorized, - "User is not allowed to upload tools.") - return - } - - // Parse our multipart form, 10 << 20 specifies a maximum - // upload of 10 MB files. - err = r.ParseMultipartForm(10 << 20) - if err != nil { - returnError(w, http.StatusBadRequest, "Unsupported params") - return - } - defer r.MultipartForm.RemoveAll() - - tool := &artifacts_proto.Tool{} - params, pres := r.Form["_params_"] - if !pres || len(params) != 1 { - returnError(w, http.StatusBadRequest, "Unsupported params") - return - } - - err = json.Unmarshal([]byte(params[0]), tool) - if err != nil { - returnError(w, http.StatusBadRequest, "Unsupported params") - return - } - - // FormFile returns the first file for the given key `myFile` - // it also returns the FileHeader so we can get the Filename, - // the Header and the size of the file - file, handler, err := r.FormFile("file") - if err != nil { - returnError(w, 403, fmt.Sprintf("Unsupported params: %v", err)) - return - } - defer file.Close() - - tool.Filename = path.Base(handler.Filename) - tool.ServeLocally = true - - file_store_factory := file_store.GetFileStore(config_obj) - path_manager := paths.NewInventoryPathManager(config_obj, tool) - writer, err := file_store_factory.WriteFile(path_manager.Path()) - if err != nil { - returnError(w, http.StatusInternalServerError, - fmt.Sprintf("Error: %v", err)) - return - } - defer writer.Close() - - err = writer.Truncate() - if err != nil { - returnError(w, http.StatusInternalServerError, - fmt.Sprintf("Error: %v", err)) - return - } - - sha_sum := sha256.New() - - _, err = io.Copy(writer, io.TeeReader(file, sha_sum)) - if err != nil { - returnError(w, http.StatusInternalServerError, - fmt.Sprintf("Error: %v", err)) - return - } - - tool.Hash = hex.EncodeToString(sha_sum.Sum(nil)) - - err = services.GetInventory().AddTool(config_obj, tool, - services.ToolOptions{ - AdminOverride: true, - }) - if err != nil { - returnError(w, http.StatusInternalServerError, - fmt.Sprintf("Error: %v", err)) - return - } - - // Now materialize the tool - tool, err = services.GetInventory().GetToolInfo( - r.Context(), config_obj, tool.Name) - if err != nil { - returnError(w, http.StatusInternalServerError, - fmt.Sprintf("Error: %v", err)) - return - } - - serialized, _ := json.Marshal(tool) - _, err = w.Write(serialized) - if err != nil { - logger := logging.GetLogger(config_obj, &logging.GUIComponent) - logger.Error("toolUploadHandler: %v", err) - } - }) +var ( + ToolError = utils.Wrap(utils.PermissionDenied, + "User is not allowed to upload tools.") +) + +func toolUploadHandler(config_obj *config_proto.Config) http.Handler { + return api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + org_id := authenticators.GetOrgIdFromRequest(r) + org_manager, err := services.GetOrgManager() + if err != nil { + returnError(config_obj, w, http.StatusUnauthorized, err) + return + } + + org_config_obj, err := org_manager.GetOrgConfig(org_id) + if err != nil { + returnError(config_obj, w, http.StatusUnauthorized, err) + return + } + + // Check for acls + userinfo := GetUserInfo(r.Context(), org_config_obj) + permissions := acls.ARTIFACT_WRITER + perm, err := services.CheckAccess(org_config_obj, userinfo.Name, permissions) + if !perm || err != nil { + returnError(config_obj, w, http.StatusUnauthorized, ToolError) + return + } + + // Parse our multipart form, 10 << 20 specifies a maximum + // upload of 10 MB files. + err = r.ParseMultipartForm(10 << 25) + if err != nil { + returnError(config_obj, w, http.StatusBadRequest, utils.InvalidArgError) + return + } + defer func() { + err := r.MultipartForm.RemoveAll() + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.FrontendComponent) + logger.Error("toolUploadHandler MultipartForm.RemoveAll: %v", err) + } + }() + + tool := &artifacts_proto.Tool{} + params, pres := r.Form["_params_"] + if !pres || len(params) != 1 { + returnError(config_obj, w, http.StatusBadRequest, utils.InvalidArgError) + return + } + + err = json.Unmarshal([]byte(params[0]), tool) + if err != nil { + returnError(config_obj, w, http.StatusBadRequest, utils.InvalidArgError) + return + } + + // FormFile returns the first file for the given key `myFile` + // it also returns the FileHeader so we can get the Filename, + // the Header and the size of the file + file, handler, err := r.FormFile("file") + if err != nil { + returnError(config_obj, w, 403, utils.InvalidArgError) + return + } + defer file.Close() + + tool.Filename = path.Base(handler.Filename) + tool.ServeLocally = true + + path_manager := paths.NewInventoryPathManager(org_config_obj, tool) + pathspec, file_store_factory, err := path_manager.Path() + if err != nil { + returnError(config_obj, w, 404, err) + } + + writer, err := file_store_factory.WriteFile(pathspec) + if err != nil { + returnError(config_obj, w, http.StatusInternalServerError, err) + return + } + defer writer.Close() + + err = writer.Truncate() + if err != nil { + returnError(config_obj, w, http.StatusInternalServerError, err) + return + } + + sha_sum := sha256.New() + + _, err = io.Copy(writer, io.TeeReader(file, sha_sum)) + if err != nil { + returnError(config_obj, w, http.StatusInternalServerError, err) + return + } + + tool.Hash = hex.EncodeToString(sha_sum.Sum(nil)) + + inventory, err := services.GetInventory(org_config_obj) + if err != nil { + returnError(config_obj, w, http.StatusInternalServerError, err) + return + } + + ctx := r.Context() + err = inventory.AddTool(ctx, org_config_obj, tool, + services.ToolOptions{ + AdminOverride: true, + }) + if err != nil { + returnError(config_obj, w, http.StatusInternalServerError, err) + return + } + + // Now materialize the tool + tool, err = inventory.GetToolInfo( + r.Context(), org_config_obj, tool.Name, tool.Version) + if err != nil { + returnError(config_obj, w, http.StatusInternalServerError, err) + return + } + + serialized, _ := json.Marshal(tool) + _, err = w.Write(serialized) + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.GUIComponent) + logger.Error("toolUploadHandler: %v", err) + } + }) } -func formUploadHandler( - config_obj *config_proto.Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check for acls - userinfo := GetUserInfo(r.Context(), config_obj) - permissions := acls.COLLECT_CLIENT - perm, err := acls.CheckAccess(config_obj, userinfo.Name, permissions) - if !perm || err != nil { - returnError(w, http.StatusUnauthorized, - "User is not allowed to upload files for forms.") - return - } - - // Parse our multipart form, 10 << 20 specifies a maximum - // upload of 10 MB files. - err = r.ParseMultipartForm(10 << 20) - if err != nil { - returnError(w, http.StatusBadRequest, "Unsupported params") - return - } - defer r.MultipartForm.RemoveAll() - - form_desc := &api_proto.FormUploadMetadata{} - params, pres := r.Form["_params_"] - if !pres || len(params) != 1 { - returnError(w, http.StatusBadRequest, "Unsupported params") - return - } - - err = json.Unmarshal([]byte(params[0]), form_desc) - if err != nil { - returnError(w, http.StatusBadRequest, "Unsupported params") - return - } - - // FormFile returns the first file for the given key `file` - // it also returns the FileHeader so we can get the Filename, - // the Header and the size of the file - file, handler, err := r.FormFile("file") - if err != nil { - returnError(w, 403, fmt.Sprintf("Unsupported params: %v", err)) - return - } - defer file.Close() - - form_desc.Filename = path.Base(handler.Filename) - - file_store_factory := file_store.GetFileStore(config_obj) - path_manager := paths.NewFormUploadPathManager( - config_obj, form_desc.Filename) - - form_desc.Url = path_manager.URL() - - writer, err := file_store_factory.WriteFile(path_manager.Path()) - if err != nil { - returnError(w, http.StatusInternalServerError, - fmt.Sprintf("Error: %v", err)) - return - } - defer writer.Close() - - err = writer.Truncate() - if err != nil { - returnError(w, http.StatusInternalServerError, - fmt.Sprintf("Error: %v", err)) - return - } - - _, err = io.Copy(writer, file) - if err != nil { - returnError(w, http.StatusInternalServerError, - fmt.Sprintf("Error: %v", err)) - return - } - - serialized, _ := json.Marshal(form_desc) - _, err = w.Write(serialized) - if err != nil { - logger := logging.GetLogger(config_obj, &logging.GUIComponent) - logger.Error("toolUploadHandler: %v", err) - } - }) +func formUploadHandler(config_obj *config_proto.Config) http.Handler { + return api_utils.HandlerFunc(nil, + func(w http.ResponseWriter, r *http.Request) { + org_id := authenticators.GetOrgIdFromRequest(r) + org_manager, err := services.GetOrgManager() + if err != nil { + returnError(config_obj, w, http.StatusUnauthorized, err) + return + } + + org_config_obj, err := org_manager.GetOrgConfig(org_id) + if err != nil { + returnError(config_obj, w, http.StatusUnauthorized, err) + return + } + + // Check for acls + userinfo := GetUserInfo(r.Context(), org_config_obj) + permissions := acls.COLLECT_CLIENT + perm, err := services.CheckAccess(org_config_obj, userinfo.Name, permissions) + if !perm || err != nil { + returnError(config_obj, w, http.StatusUnauthorized, ToolError) + return + } + + // Parse our multipart form, 10 << 20 specifies a maximum + // upload of 10 MB files. + err = r.ParseMultipartForm(10 << 20) + if err != nil { + returnError(config_obj, w, http.StatusBadRequest, utils.InvalidArgError) + return + } + defer func() { + err := r.MultipartForm.RemoveAll() + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.GUIComponent) + logger.Error("formUploadHandler MultipartForm.RemoveAll: %v", err) + } + }() + + form_desc := &api_proto.FormUploadMetadata{} + params, pres := r.Form["_params_"] + if !pres || len(params) != 1 { + returnError(config_obj, w, http.StatusBadRequest, utils.InvalidArgError) + return + } + + err = json.Unmarshal([]byte(params[0]), form_desc) + if err != nil { + returnError(config_obj, w, http.StatusBadRequest, utils.InvalidArgError) + return + } + + // FormFile returns the first file for the given key `file` + // it also returns the FileHeader so we can get the Filename, + // the Header and the size of the file + file, handler, err := r.FormFile("file") + if err != nil { + returnError(config_obj, w, 403, + fmt.Errorf("%w: %v", utils.InvalidArgError, err)) + return + } + defer file.Close() + + form_desc.Filename = path.Base(handler.Filename) + + path_manager := paths.NewFormUploadPathManager( + org_config_obj, form_desc.Filename) + + pathspec, file_store_factory, err := path_manager.Path() + if err != nil { + returnError(config_obj, w, 403, err) + return + } + + form_desc.Url = path_manager.URL() + form_desc.VfsPath = pathspec.Components() + + writer, err := file_store_factory.WriteFile(pathspec) + if err != nil { + returnError(config_obj, w, http.StatusInternalServerError, err) + return + } + defer writer.Close() + + err = writer.Truncate() + if err != nil { + returnError(config_obj, w, http.StatusInternalServerError, err) + return + } + + _, err = io.Copy(writer, file) + if err != nil { + returnError(config_obj, w, http.StatusInternalServerError, err) + return + } + + serialized, _ := json.Marshal(form_desc) + _, err = w.Write(serialized) + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.GUIComponent) + logger.Error("toolUploadHandler: %v", err) + } + }) } diff --git a/api/users.go b/api/users.go index e5425aa1d..00d02bb20 100644 --- a/api/users.go +++ b/api/users.go @@ -1,49 +1,322 @@ package api import ( - context "golang.org/x/net/context" + "context" + "errors" + "sort" + + "github.com/Velocidex/ordereddict" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" "www.velocidex.com/golang/velociraptor/acls" + acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" api_proto "www.velocidex.com/golang/velociraptor/api/proto" - users "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" ) +// This is only used to set the user's own password which is always +// allowed for any user. +func (self *ApiServer) SetPassword( + ctx context.Context, + in *api_proto.SetPasswordRequest) (*emptypb.Empty, error) { + + defer Instrument("SetPassword")() + + // Enforce a minimum length password + if len(in.Password) < 4 { + return nil, InvalidStatus("Password is not set or too short") + } + + user_manager := services.GetUserManager() + user_record, _, err := user_manager.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + org_manager, err := services.GetOrgManager() + if err != nil { + return nil, Status(self.verbose, err) + } + + org_config_obj, err := org_manager.GetOrgConfig(services.ROOT_ORG_ID) + if err != nil { + return nil, Status(self.verbose, err) + } + + // The user we change the password for. + target := in.Username + if target == "" { + target = principal + } + + err = user_manager.SetUserPassword( + ctx, org_config_obj, principal, target, in.Password, "") + if err != nil { + return nil, Status(self.verbose, err) + } + + return &emptypb.Empty{}, nil +} + func (self *ApiServer) GetUsers( ctx context.Context, in *emptypb.Empty) (*api_proto.Users, error) { - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - user_record, err := users.GetUser(self.config, user_name) + defer Instrument("GetUsers")() + + user_manager := services.GetUserManager() + user_record, org_config_obj, err := user_manager.GetUserFromContext(ctx) if err != nil { - return nil, err + return nil, Status(self.verbose, err) } - permissions := acls.READ_RESULTS - perm, err := acls.CheckAccess(self.config, user_record.Name, permissions) - if !perm || err != nil { - return nil, status.Error(codes.PermissionDenied, - "User is not allowed to enumerate users.") + principal := user_record.Name + + // Only show users in the current org + users, err := user_manager.ListUsers(ctx, principal, []string{org_config_obj.OrgId}) + if err != nil { + return nil, Status(self.verbose, err) + } + + sort.Slice(users, func(i, j int) bool { return users[i].Name < users[j].Name }) + return &api_proto.Users{Users: users}, nil +} + +func (self *ApiServer) GetGlobalUsers( + ctx context.Context, + in *emptypb.Empty) (*api_proto.Users, error) { + + defer Instrument("GetGlobalUsers")() + + user_manager := services.GetUserManager() + user_record, _, err := user_manager.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + principal := user_record.Name + + // Show all users visible to us + users, err := user_manager.ListUsers(ctx, principal, []string{}) + if err != nil { + return nil, Status(self.verbose, err) + } + + sort.Slice(users, func(i, j int) bool { return users[i].Name < users[j].Name }) + return &api_proto.Users{Users: users}, nil +} + +// Create a new user in the specified orgs. +func (self *ApiServer) CreateUser(ctx context.Context, + in *api_proto.UpdateUserRequest) (*emptypb.Empty, error) { + + defer Instrument("CreateUser")() + + users_manager := services.GetUserManager() + user_record, org_config_obj, err := users_manager.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + principal := user_record.Name + + // Prepare an ACL object from the incoming request. + acl := &acl_proto.ApiClientACL{ + Roles: in.Roles, + } + + mode := services.UseExistingUser + if in.AddNewUser { + mode = services.AddNewUser + } + + err = users_manager.AddUserToOrg(ctx, mode, principal, in.Name, in.Orgs, acl) + + if err == nil { + err := services.LogAudit(ctx, + org_config_obj, principal, "user_create", + ordereddict.NewDict(). + Set("username", in.Name). + Set("acl", acl). + Set("org_ids", in.Orgs)) + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.FrontendComponent) + logger.Error("user_create %v %v", principal, in.Name) + } + } - result := &api_proto.Users{} + return &emptypb.Empty{}, err +} + +func (self *ApiServer) GetUser( + ctx context.Context, in *api_proto.UserRequest) (*api_proto.VelociraptorUser, error) { + + defer Instrument("GetUser")() - users, err := users.ListUsers(self.config) + users_manager := services.GetUserManager() + user_record, _, err := users_manager.GetUserFromContext(ctx) if err != nil { return nil, err } - result.Users = users + user, err := users_manager.GetUser(ctx, user_record.Name, in.Name) + if err != nil { + if errors.Is(err, acls.PermissionDenied) { + return nil, status.Error(codes.PermissionDenied, + "User is not allowed to view requested user.") + } + return nil, err + } - return result, nil + return user, nil } func (self *ApiServer) GetUserFavorites( ctx context.Context, in *api_proto.Favorite) (*api_proto.Favorites, error) { + defer Instrument("GetUserFavorites")() + // No special permission requires to view a user's own favorites. - user_name := GetGRPCUserInfo(self.config, ctx, self.ca_pool).Name - return users.GetFavorites(self.config, user_name, in.Type) + users_manager := services.GetUserManager() + user_record, org_config_obj, err := users_manager.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + return users_manager.GetFavorites(ctx, org_config_obj, principal, in.Type) +} + +func (self *ApiServer) GetUserRoles( + ctx context.Context, + in *api_proto.UserRequest) (*api_proto.UserRoles, error) { + + defer Instrument("GetUserRoles")() + + users_manager := services.GetUserManager() + user_record, org_config_obj, err := users_manager.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + // Allow the user to ask about other orgs. + if !utils.CompareOrgIds(in.Org, org_config_obj.OrgId) { + org_manager, err := services.GetOrgManager() + if err != nil { + return nil, Status(self.verbose, err) + } + + org_config_obj, err = org_manager.GetOrgConfig(in.Org) + if err != nil { + return nil, Status(self.verbose, err) + } + } + + // Users need at least read access to see other users in the org. + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + // If the user is org admin they can do anything in any org. + org_manager, err := services.GetOrgManager() + if err != nil { + return nil, Status(self.verbose, err) + } + root_config_obj, err := org_manager.GetOrgConfig(services.ROOT_ORG_ID) + if err != nil { + return nil, Status(self.verbose, err) + } + permissions := acls.ORG_ADMIN + perm, err := services.CheckAccess( + root_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, "User is not allowed to access org.") + } + } + + acl_manager, err := services.GetACLManager(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + policy, err := acl_manager.GetPolicy(org_config_obj, in.Name) + if err != nil { + policy = &acl_proto.ApiClientACL{} + } + + user_roles := &api_proto.UserRoles{ + Name: in.Name, + Org: in.Org, + OrgName: org_config_obj.OrgName, + Roles: policy.Roles, + Permissions: acls.DescribePermissions(policy), + AllRoles: acls.ALL_ROLES, + AllPermissions: acls.ALL_PERMISSIONS, + } + + // Expand the policy's permissions + err = acls.GetRolePermissions(org_config_obj, policy.Roles, policy) + if err != nil { + return nil, Status(self.verbose, err) + } + + user_roles.EffectivePermissions = acls.DescribePermissions(policy) + + return user_roles, nil +} + +func (self *ApiServer) SetUserRoles( + ctx context.Context, + in *api_proto.UserRoles) (*emptypb.Empty, error) { + + defer Instrument("SetUserRoles")() + + users_manager := services.GetUserManager() + user_record, org_config_obj, err := users_manager.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + principal := user_record.Name + + // Prepare an ACL object from the incoming request. + acl := &acl_proto.ApiClientACL{} + + for _, r := range in.Roles { + if acls.ValidateRole(r) { + acl.Roles = append(acl.Roles, r) + } + } + + // Add any special permissions + err = acls.SetTokenPermission(acl, in.Permissions...) + if err != nil { + return nil, Status(self.verbose, err) + } + + // Now attempt to set the ACL - permission checks are done by + // users.AddUserToOrg + err = users_manager.AddUserToOrg(ctx, services.UseExistingUser, + principal, in.Name, []string{in.Org}, acl) + + if err == nil { + err := services.LogAudit(ctx, + org_config_obj, principal, "user_grant", + ordereddict.NewDict(). + Set("username", in.Name). + Set("acl", acl). + Set("org_ids", []string{in.Org})) + if err != nil { + logger := logging.GetLogger(org_config_obj, &logging.FrontendComponent) + logger.Error("user_grant %v %v", principal, in.Name) + } + + } + + return &emptypb.Empty{}, err } diff --git a/api/utils/grpc.go b/api/utils/grpc.go new file mode 100644 index 000000000..834a0a7a9 --- /dev/null +++ b/api/utils/grpc.go @@ -0,0 +1,29 @@ +package utils + +import ( + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// https://github.com/grpc/grpc-go/issues/1229#issuecomment-302755717 +// DoWithTimeout runs f and returns its error. If the deadline d +// elapses first, it returns a grpc DeadlineExceeded error instead. +func DoWithTimeout(f func() error, d time.Duration) error { + errChan := make(chan error, 1) + go func() { + errChan <- f() + close(errChan) + }() + t := time.NewTimer(d) + select { + case <-t.C: + return status.Errorf(codes.DeadlineExceeded, "too slow") + case err := <-errChan: + if !t.Stop() { + <-t.C + } + return err + } +} diff --git a/api/utils/mux.go b/api/utils/mux.go new file mode 100644 index 000000000..00692d674 --- /dev/null +++ b/api/utils/mux.go @@ -0,0 +1,122 @@ +package utils + +import ( + "fmt" + "net/http" + "path/filepath" + "runtime" + "sort" + "strings" + + "github.com/Velocidex/ordereddict" +) + +type Stringer interface { + String() string +} + +type ServeMux struct { + *http.ServeMux + + Handlers map[string]http.Handler +} + +func (self *ServeMux) Handle(pattern string, handler http.Handler) { + self.Handlers[pattern] = handler + self.ServeMux.Handle(pattern, handler) +} + +func (self *ServeMux) Debug() *ordereddict.Dict { + res := ordereddict.NewDict() + var keys []string + for k := range self.Handlers { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, k := range keys { + v, ok := self.Handlers[k] + if !ok { + continue + } + + name := fmt.Sprintf("%T", v) + stringer, ok := v.(Stringer) + if ok { + name = stringer.String() + } + + parts := strings.Split(name, ":") + res.Set(k, parts) + } + return res +} + +func NewServeMux() *ServeMux { + return &ServeMux{ + ServeMux: http.NewServeMux(), + Handlers: make(map[string]http.Handler), + } +} + +type HandlerFuncContainer struct { + http.HandlerFunc + callSite string + parent *HandlerFuncContainer +} + +func (self *HandlerFuncContainer) String() string { + res := self.callSite + if self.parent != nil { + res += ": " + self.parent.String() + } + + return res +} + +func (self *HandlerFuncContainer) AddChild(note string) *HandlerFuncContainer { + if self.callSite != "" { + child := &HandlerFuncContainer{ + HandlerFunc: self.HandlerFunc, + parent: self, + callSite: note, + } + return child + } + self.callSite = note + return self +} + +func HandlerFunc(parent http.Handler, f http.HandlerFunc) *HandlerFuncContainer { + res := &HandlerFuncContainer{ + HandlerFunc: http.HandlerFunc(f), + } + + if parent != nil { + parent_handler, ok := parent.(*HandlerFuncContainer) + if ok { + res.parent = parent_handler + } else { + res.parent = &HandlerFuncContainer{ + callSite: fmt.Sprintf("%T", parent), + } + } + } + + pc, _, _, ok := runtime.Caller(1) + if ok { + details := runtime.FuncForPC(pc) + if details != nil { + res.callSite = filepath.Base(details.Name()) + } + } + + return res +} + +func StripPrefix(prefix string, h http.Handler) http.Handler { + handler := http.StripPrefix(prefix, h) + + return HandlerFunc(h, handler.ServeHTTP) +} diff --git a/api/utils/utils.go b/api/utils/utils.go new file mode 100644 index 000000000..44fba2557 --- /dev/null +++ b/api/utils/utils.go @@ -0,0 +1,101 @@ +package utils + +import ( + "strings" + + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/services" +) + +// Normalize the base path. If base path is not specified or / return +// "". Otherwise ensure base path has a leading / and no following / +func GetBasePath(config_obj *config_proto.Config, parts ...string) string { + frontend_service, err := services.GetFrontendManager(config_obj) + if err != nil { + return "/" + } + base, _ := frontend_service.GetBaseURL(config_obj) + + args := append([]string{base.Path}, parts...) + base.Path = Join(args...) + if base.Path == "/" { + return "" + } + + return base.Path +} + +// Return the base directory (with the trailing /) for the base path +func GetBaseDirectory(config_obj *config_proto.Config) string { + base := GetBasePath(config_obj) + return strings.TrimSuffix(base, "/") + "/" +} + +// Returns the fully qualified URL to the API endpoint. +func GetPublicURL(config_obj *config_proto.Config, parts ...string) string { + frontend_service, err := services.GetFrontendManager(config_obj) + if err != nil { + return "/" + } + base, err := frontend_service.GetBaseURL(config_obj) + if err != nil { + return "" + } + + args := append([]string{base.Path}, parts...) + base.Path = Join(args...) + return base.String() +} + +// Returns the absolute public URL referring to all the parts +func PublicURL(config_obj *config_proto.Config, parts ...string) string { + frontend_service, err := services.GetFrontendManager(config_obj) + if err != nil { + return "/" + } + base, err := frontend_service.GetBaseURL(config_obj) + if err != nil { + return "/" + } + args := append([]string{base.Path}, parts...) + base.Path = Join(args...) + return base.String() +} + +// Join all parts of the URL to make sure that there is only a single +// / between them regardless of if they have leading or trailing /. +// Ensure the url starts with / unless it is an absolute URL starting +// with http If the final part ends with / preserve that to refer to a +// directory. +func Join(parts ...string) string { + if len(parts) == 0 { + return "/" + } + + result := []string{} + for _, p := range parts { + p = strings.TrimPrefix(p, "/") + p = strings.TrimSuffix(p, "/") + if p != "" { + result = append(result, p) + } + } + + res := strings.Join(result, "/") + // If the last part ends with / preserve that + if strings.HasSuffix(parts[len(parts)-1], "/") { + res += "/" + } + + // Ensure the URL starts with / + if !strings.HasPrefix(res, "/") && !strings.HasPrefix(res, "http") { + res = "/" + res + } + + return res +} + +func Homepage(config_obj *config_proto.Config) string { + base := GetBasePath(config_obj) + return Join(base, "/app/index.html") +} diff --git a/api/vfs.go b/api/vfs.go index 677c93330..9030581be 100644 --- a/api/vfs.go +++ b/api/vfs.go @@ -1,6 +1,6 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. + Velociraptor - Dig Deeper + Copyright (C) 2019-2025 Rapid7 Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -62,211 +62,19 @@ GetVFSDownloadInfoPath(). package api import ( + "context" "fmt" "strings" - context "golang.org/x/net/context" + "www.velocidex.com/golang/velociraptor/acls" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" api_proto "www.velocidex.com/golang/velociraptor/api/proto" - config_proto "www.velocidex.com/golang/velociraptor/config/proto" - datastore "www.velocidex.com/golang/velociraptor/datastore" flows_proto "www.velocidex.com/golang/velociraptor/flows/proto" "www.velocidex.com/golang/velociraptor/json" - "www.velocidex.com/golang/velociraptor/paths" + "www.velocidex.com/golang/velociraptor/services" "www.velocidex.com/golang/velociraptor/utils" ) -type FileInfoRow struct { - Name string `json:"Name"` - Size int64 `json:"Size"` - Timestamp string `json:"Timestamp"` - Mode string `json:"Mode"` - Download *flows_proto.VFSDownloadInfo `json:"Download"` - Mtime string `json:"mtime"` - Atime string `json:"atime"` - Ctime string `json:"ctime"` - FullPath string `json:"_FullPath"` - Data interface{} `json:"_Data"` -} - -// Render the root level pseudo directory. This provides anchor points -// for the other drivers in the navigation. -func renderRootVFS(client_id string) *api_proto.VFSListResponse { - return &api_proto.VFSListResponse{ - Response: ` - [ - {"Mode": "drwxrwxrwx", "Name": "file"}, - {"Mode": "drwxrwxrwx", "Name": "ntfs"}, - {"Mode": "drwxrwxrwx", "Name": "registry"} - ]`, - } -} - -// Render VFS nodes with VQL collection + uploads. -func renderDBVFS( - config_obj *config_proto.Config, - client_id string, - components []string) (*api_proto.VFSListResponse, error) { - - db, err := datastore.GetDB(config_obj) - if err != nil { - return nil, err - } - - path_manager := paths.NewClientPathManager(client_id) - - // Figure out where the download info files are. - download_info_path := path_manager.VFSDownloadInfoPath(components) - downloaded_files, _ := db.ListChildren(config_obj, download_info_path) - - result := &api_proto.VFSListResponse{} - - // Figure out where the directory info is. - vfs_path := path_manager.VFSPath(components) - - // If file does not exist, we have an empty response - _ = db.GetSubject(config_obj, vfs_path, result) - - // Empty responses mean the directory is empty - no need to - // worry about downloads. - json_response := result.Response - if json_response == "" { - return result, nil - } - - // Merge uploaded file info with the VFSListResponse. Note - // that if there are no downloaded files, we just pass the - // VFSListResponse lazily to the caller. - if len(downloaded_files) > 0 { - lookup := make(map[string]bool) - for _, filename := range downloaded_files { - lookup[filename.Base()] = true - } - - var rows []map[string]interface{} - err := json.Unmarshal([]byte(json_response), &rows) - if err != nil { - return nil, err - } - - // If the row refers to a downloaded file, we mark it - // with the download details. - for _, row := range rows { - name, ok := row["Name"].(string) - if !ok { - continue - } - - _, pres := lookup[name] - if !pres { - continue - } - - // Make a copy for each path - file_components := download_info_path.AddChild(name) - download_info := &flows_proto.VFSDownloadInfo{} - err := db.GetSubject( - config_obj, file_components, download_info) - if err == nil { - // Support reading older - // VFSDownloadInfo protobufs which - // only contained the vfs_path and not - // the components. - if download_info.VfsPath != "" { - download_info.Components = utils.SplitComponents(download_info.VfsPath) - } - - row["Download"] = download_info - } - } - - encoded_rows, err := json.MarshalIndent(rows) - if err != nil { - return nil, err - } - - result.Response = string(encoded_rows) - } - - // Add a Download column as the first column. - result.Columns = append([]string{"Download"}, result.Columns...) - result.Types = append(result.Types, &actions_proto.VQLTypeMap{ - Column: "Download", - Type: "Download", - }) - - return result, nil -} - -func vfsListDirectory( - config_obj *config_proto.Config, - client_id string, - components []string) (*api_proto.VFSListResponse, error) { - - if len(components) == 0 { - return renderRootVFS(client_id), nil - } - - return renderDBVFS(config_obj, client_id, components) -} - -// NOTE: We only support stat of DBFS style entries. This function is -// used to track when a directory changes in response to a refresh -// directory flow. -func vfsStatDirectory( - config_obj *config_proto.Config, - client_id string, - vfs_components []string) (*api_proto.VFSListResponse, error) { - - db, err := datastore.GetDB(config_obj) - if err != nil { - return nil, err - } - - path_manager := paths.NewClientPathManager(client_id) - result := &api_proto.VFSListResponse{} - - // Regardless of error we return success - if the file does - // not exist yet then it will have no flow id associated with - // it. This allows the gui to watch for the VFS directory to - // appear for the first time. - _ = db.GetSubject(config_obj, - path_manager.VFSPath(vfs_components), result) - - // Remove the actual response which might be large. - result.Response = "" - - return result, nil -} - -func vfsStatDownload( - config_obj *config_proto.Config, - client_id string, - accessor string, - path_components []string) (*flows_proto.VFSDownloadInfo, error) { - - path_spec := paths.NewClientPathManager(client_id). - VFSDownloadInfoPath(path_components) - - db, err := datastore.GetDB(config_obj) - if err != nil { - return nil, err - } - - result := &flows_proto.VFSDownloadInfo{} - - // Regardless of error we return success - if the file does - // not exist yet then it will have no flow id associated with - // it. This allows the gui to watch for the VFS directory to - // appear for the first time. - err = db.GetSubject(config_obj, path_spec, result) - if err != nil { - return nil, err - } - - return result, nil -} - // Split the vfs path into a client path and an accessor. We only // support certain well defined prefixes which control the type of // accessor to use. @@ -281,7 +89,7 @@ func GetClientPath(components []string) (client_path string, accessor string) { } switch components[0] { - case "file", "registry": + case "auto", "file", "registry": return utils.JoinComponents(components[1:], "/"), components[0] case "ntfs": @@ -302,10 +110,16 @@ func vfsRefreshDirectory( vfs_components []string, depth uint64) (*flows_proto.ArtifactCollectorResponse, error) { + var components string + if len(vfs_components) > 0 { + components = json.MustMarshalString(vfs_components[1:]) + } + client_path, accessor := GetClientPath(vfs_components) request := MakeCollectorRequest( client_id, "System.VFS.ListDirectory", "Path", client_path, + "Components", components, "Accessor", accessor, "Depth", fmt.Sprintf("%v", depth)) @@ -315,3 +129,93 @@ func vfsRefreshDirectory( result, err := self.CollectArtifact(ctx, request) return result, err } + +// Read the file listing table, but enrich the result with download +// info. +func (self *ApiServer) VFSListDirectoryFiles( + ctx context.Context, + in *api_proto.GetTableRequest) (*api_proto.GetTableResponse, error) { + + defer Instrument("VFSListDirectoryFiles")() + + users := services.GetUserManager() + user_record, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + principal := user_record.Name + + permissions := acls.READ_RESULTS + perm, err := services.CheckAccess(org_config_obj, principal, permissions) + if !perm || err != nil { + return nil, PermissionDenied(err, + "User is not allowed to view the VFS.") + } + + vfs_service, err := services.GetVFSService(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + result, err := vfs_service.ListDirectoryFiles(ctx, org_config_obj, in) + if err != nil { + return nil, Status(self.verbose, err) + } + + return result, nil +} + +func (self *ApiServer) VFSDownloadFile( + ctx context.Context, + in *api_proto.VFSStatDownloadRequest) (*api_proto.StartFlowResponse, error) { + + defer Instrument("VFSDownloadFile")() + + users := services.GetUserManager() + _, org_config_obj, err := users.GetUserFromContext(ctx) + if err != nil { + return nil, Status(self.verbose, err) + } + + request := &flows_proto.ArtifactCollectorArgs{ + ClientId: in.ClientId, + Urgent: true, + Artifacts: []string{"System.VFS.DownloadFile"}, + Specs: []*flows_proto.ArtifactSpec{{ + Artifact: "System.VFS.DownloadFile", + Parameters: &flows_proto.ArtifactParameters{ + Env: []*actions_proto.VQLEnv{{ + Key: "Components", + Value: json.MustMarshalString(in.Components), + }, { + Key: "Accessor", + Value: in.Accessor, + }}, + }, + }}, + } + + resp, err := self.CollectArtifact(ctx, request) + if err != nil { + return nil, Status(self.verbose, err) + } + + vfs_service, err := services.GetVFSService(org_config_obj) + if err != nil { + return nil, Status(self.verbose, err) + } + + err = vfs_service.WriteDownloadInfo(ctx, org_config_obj, in.ClientId, + in.Accessor, in.Components, &flows_proto.VFSDownloadInfo{ + FlowId: resp.FlowId, + Mtime: uint64(utils.GetTime().Now().UnixNano() / 1000), + InFlight: true, + }) + if err != nil { + return nil, Status(self.verbose, err) + } + + return &api_proto.StartFlowResponse{ + FlowId: resp.FlowId, + }, nil +} diff --git a/api/vql.go b/api/vql.go index aabcc7088..9afd86b14 100644 --- a/api/vql.go +++ b/api/vql.go @@ -1,31 +1,33 @@ /* - Velociraptor - Hunting Evil - Copyright (C) 2019 Velocidex Innovations. +Velociraptor - Dig Deeper +Copyright (C) 2019-2025 Rapid7 Inc. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package api import ( + "context" + "github.com/Velocidex/ordereddict" - context "golang.org/x/net/context" api_proto "www.velocidex.com/golang/velociraptor/api/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" - "www.velocidex.com/golang/velociraptor/file_store/csv" + "www.velocidex.com/golang/velociraptor/json" + vjson "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/services" - vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" "www.velocidex.com/golang/vfilter" ) @@ -38,14 +40,14 @@ func RunVQL( result := &api_proto.GetTableResponse{} - manager, err := services.GetRepositoryManager() + manager, err := services.GetRepositoryManager(config_obj) if err != nil { return nil, err } scope := manager.BuildScope(services.ScopeBuilder{ Config: config_obj, Env: env, - ACLManager: vql_subsystem.NewServerACLManager(config_obj, principal), + ACLManager: acl_managers.NewServerACLManager(config_obj, principal), Logger: logging.NewPlainLogger(config_obj, &logging.ToolComponent), }) defer scope.Close() @@ -63,16 +65,23 @@ func RunVQL( result.Columns = scope.GetMembers(row) } - new_row := &api_proto.Row{} + new_row := make([]interface{}, 0, len(result.Columns)) for _, column := range result.Columns { value, pres := scope.Associative(row, column) if !pres { value = "" } - new_row.Cell = append(new_row.Cell, csv.AnyToString(value)) + new_row = append(new_row, value) } - result.Rows = append(result.Rows, new_row) + opts := vjson.DefaultEncOpts() + serialized, err := json.MarshalWithOptions(new_row, opts) + if err != nil { + continue + } + result.Rows = append(result.Rows, &api_proto.Row{ + Json: string(serialized), + }) } return result, nil diff --git a/artifacts/definitions/Admin/Client/Remove.yaml b/artifacts/definitions/Admin/Client/Remove.yaml index b2e6408bb..4c7944ca6 100644 --- a/artifacts/definitions/Admin/Client/Remove.yaml +++ b/artifacts/definitions/Admin/Client/Remove.yaml @@ -11,15 +11,17 @@ parameters: - name: Age description: Remove clients older than this many days default: "7" + type: int - name: ReallyDoIt type: bool sources: - query: | + LET Threshold <= timestamp(epoch=now() - Age * 3600 * 24 ) LET old_clients = SELECT os_info.fqdn AS Fqdn, client_id, - timestamp(epoch=last_seen_at/1000000) AS LastSeen FROM clients() - WHERE LastSeen < now() - ( atoi(string=Age) * 3600 * 24 ) + timestamp(epoch=last_seen_at) AS LastSeen FROM clients() + WHERE LastSeen < Threshold SELECT * FROM foreach(row=old_clients, query={ diff --git a/artifacts/definitions/Admin/Client/Uninstall.yaml b/artifacts/definitions/Admin/Client/Uninstall.yaml index 4baa4dbf6..5170c8316 100644 --- a/artifacts/definitions/Admin/Client/Uninstall.yaml +++ b/artifacts/definitions/Admin/Client/Uninstall.yaml @@ -25,16 +25,70 @@ parameters: type: bool sources: - - precondition: + - name: Windows + precondition: SELECT OS From info() where OS = 'windows' query: | - LET packages = SELECT Name, DisplayName FROM Artifact.Windows.Sys.Programs() + LET packages = SELECT KeyName, DisplayName,UninstallString + FROM Artifact.Windows.Sys.Programs() WHERE DisplayName =~ DisplayNameRegex AND log(message="Will uninstall " + DisplayName) - LET uninstall(Name) = SELECT * FROM execve(argv=['msiexec', '/quiet', '/x', Name]) + LET uninstall(UninstallString) = SELECT * FROM execve( + argv=commandline_split(command=UninstallString) + "/quiet") - SELECT Name, DisplayName, - if(condition=ReallyDoIt, then=uninstall(Name=Name).Stdout) AS UninstallLog + SELECT KeyName, DisplayName, UninstallString, + if(condition=ReallyDoIt, + then=uninstall(UninstallString=UninstallString).Stdout) AS UninstallLog FROM packages + + - name: Debian + precondition: | + -- Only run if dpkg is installed. + SELECT OS, { + SELECT ReturnCode FROM execve(argv=["dpkg", "--help"]) + } AS ReturnCode + FROM info() + WHERE OS = 'linux' AND ReturnCode = 0 + + query: | + SELECT * FROM if(condition=ReallyDoIt, + then={ + SELECT * FROM execve(argv=["dpkg", "--remove", "velociraptor-client"]) + }) + + - name: RPMBased + precondition: | + -- Only run if rpm is installed. + SELECT OS, { + SELECT ReturnCode FROM execve(argv=["rpm", "--help"]) + } AS ReturnCode + FROM info() + WHERE OS = 'linux' AND ReturnCode = 0 + + query: | + SELECT * FROM if(condition=ReallyDoIt, + then={ + SELECT * FROM switch(a={ + SELECT * FROM execve(argv=["rpm", "--erase", "velociraptor-client"]) + WHERE ReturnCode = 0 + }, b={ + // Support older clients which named the package in this way. + SELECT * FROM execve(argv=["rpm", "--erase", "velociraptor_client"]) + }) + }) + + - name: MacOS + precondition: | + SELECT OS + FROM info() + WHERE OS = 'darwin' + + query: | + LET me <= SELECT Exe FROM info() + + SELECT * FROM if(condition=ReallyDoIt, + then={ + SELECT * FROM execve(argv=[me[0].Exe, "service", "remove"]) + }) diff --git a/artifacts/definitions/Admin/Client/UpdateClientConfig.yaml b/artifacts/definitions/Admin/Client/UpdateClientConfig.yaml new file mode 100644 index 000000000..1b666183a --- /dev/null +++ b/artifacts/definitions/Admin/Client/UpdateClientConfig.yaml @@ -0,0 +1,74 @@ +name: Admin.Client.UpdateClientConfig +description: | + Sometimes we wish to move a client from one org ID to another. This + requires updating the config on the client and rekeying the client. + + This artifact will replace the client's config file and restart + it. The config file will be verified before replacing it. If set to + not rekey, the client will retain its client id but will be killed - + the service manager should restart it and cause the new config to + reload. + + This artifact has a notebook suggestion that allows a client to be + changed to a different org. + +parameters: + - name: ConfigYaml + description: The new config to write in yaml form. + - name: ConfigPath + description: Path of config file to overwrite + - name: WaitPeriod + type: int + default: 10 + - name: RekeyClient + type: bool + default: Y + description: Should the client rekey its client ID. + +required_permissions: + - EXECVE + - FILESYSTEM_WRITE + +sources: + - query: | + + LET ValidateConfig(Config) = Config.Client.server_urls + AND Config.Client.ca_certificate =~ "(?ms)-----BEGIN CERTIFICATE-----.+-----END CERTIFICATE-----" + AND Config.Client.nonce + + LET ExpandedConfigPath = expand(path=ConfigPath) + LET CheckConfigPath(ConfigPath) = SELECT * FROM stat(filename=ConfigPath) + LET Config <= parse_yaml(accessor="data", filename=ConfigYaml) + + LET DoIt = if(condition=ValidateConfig(Config=Config), + else=log(level="ERROR", message="Config is invalid") AND FALSE, + then=if(condition=CheckConfigPath(ConfigPath=ExpandedConfigPath).OSPath, + else=log(level="ERROR", + message="Config Path %v is invalid", + args=ExpandedConfigPath) AND FALSE, + then=copy(accessor="data", filename=ConfigYaml, dest=ExpandedConfigPath) + AND if(condition= RekeyClient, + then=log(message="Rekeying in %v seconds ", args=WaitPeriod) + AND rekey(wait=WaitPeriod), + else=pskill(pid=getpid())) + )) + + SELECT DoIt AS Success FROM scope() + + notebook: + - name: Move a client to a different OrgId + type: vql_suggestion + template: | + + LET ClientId = "C.622d19ea21109231" + LET RequiredOrgId = "O123" + LET ConfigPath = "%ProgramFiles%/Velociraptor/client.config.yaml" + + SELECT _client_config AS Config, OrgId , + collect_client(artifacts="Admin.Client.UpdateClientConfig", + client_id=ClientId, + env=dict(ConfigYaml=_client_config, + ConfigPath=ConfigPath)) + FROM orgs() + WHERE OrgId = RequiredOrgId + LIMIT 1 diff --git a/artifacts/definitions/Admin/Client/Upgrade.yaml b/artifacts/definitions/Admin/Client/Upgrade.yaml deleted file mode 100644 index 2d39294c2..000000000 --- a/artifacts/definitions/Admin/Client/Upgrade.yaml +++ /dev/null @@ -1,45 +0,0 @@ -name: Admin.Client.Upgrade -description: | - Remotely push new client updates. - - NOTE: This artifact requires that you supply a client MSI using the - tools interface. Simply click on the tool in the GUI and upload a - pre-packaged MSI. - - While typically the MSI will contain the Velociraptor windows - client, you can install any other MSI as well by customizing this - artifact or uploading a different msi file. - -tools: - - name: WindowsMSI - -parameters: - - name: SleepDuration - default: "600" - type: int - description: | - The MSI file is typically very large and we do not want to - overwhelm the server so we stagger the download over this many - seconds. - -sources: - - precondition: - SELECT OS From info() where OS = 'windows' - - query: | - // Force the file to be copied to the real temp directory since - // we are just about to remove the Tools directory. - LET bin <= SELECT copy(filename=FullPath, - dest=expand(path="%SYSTEMROOT%\\Temp\\") + basename(path=FullPath)) AS Dest - FROM Artifact.Generic.Utils.FetchBinary( - ToolName="WindowsMSI", IsExecutable=FALSE, - SleepDuration=SleepDuration) - - // Call the binary and return all its output in a single row. - // If we fail to download the binary we do not run the command. - SELECT * FROM foreach(row=bin, - query={ - SELECT * FROM execve( - argv=["msiexec.exe", "/i", Dest, "/q"], - length=10000000) - }) diff --git a/artifacts/definitions/Admin/Client/Upgrade/Debian.yaml b/artifacts/definitions/Admin/Client/Upgrade/Debian.yaml new file mode 100644 index 000000000..87922ad28 --- /dev/null +++ b/artifacts/definitions/Admin/Client/Upgrade/Debian.yaml @@ -0,0 +1,63 @@ +name: Admin.Client.Upgrade.Debian +description: | + Remotely push new client updates to Debian hosts. + + NOTE: This artifact requires that you supply a client Debian package by using the + tools interface or by using the "debian client" command. Simply click on the tool + in the GUI and upload a package. + +tools: + - name: VelociraptorDebian + +parameters: + - name: SleepDuration + default: "600" + type: int + description: | + The package is typically large and we do not want to + overwhelm the server so we stagger the download over this many + seconds. + + - name: ServiceName + default: "velociraptor_client" + type: str + description: | + The name of the service to restart after the upgrade. + +implied_permissions: + - EXECVE + - FILESYSTEM_WRITE + +sources: + - precondition: + SELECT OS From info() where OS =~ 'linux' + + query: | + // FetchBinary downloads to /tmp on linux + LET bin <= SELECT OSPath AS Dest + FROM Artifact.Generic.Utils.FetchBinary( + ToolName="VelociraptorDebian", IsExecutable=FALSE, + SleepDuration=SleepDuration) + + // Version handling for older clients. + LET Rm(X) = if( + condition=version(function='rm')!=NULL, + then=rm(filename=X), + else={ SELECT * FROM execve(argv=["rm", "-f", X]) }) + + // Call the binary and return all its output in a single row. + // If we fail to download the binary we do not run the command. + SELECT * FROM foreach(row=bin, + query={ + SELECT * FROM chain( + // Remove the existing prerm - Previous versions had a bug that + // would shutdown the service during uninstall. See #3122 + a={SELECT * FROM Rm(X="/var/lib/dpkg/info/velociraptor-client.prerm")}, + + // Install the new client + b={SELECT * FROM execve(argv=["dpkg", "-i", str(str=Dest)])}, + + // Restart the client + c={SELECT * FROM execve(argv=["systemctl", "restart", ServiceName])} + ) + }) diff --git a/artifacts/definitions/Admin/Client/Upgrade/RedHat.yaml b/artifacts/definitions/Admin/Client/Upgrade/RedHat.yaml new file mode 100644 index 000000000..466ac18d8 --- /dev/null +++ b/artifacts/definitions/Admin/Client/Upgrade/RedHat.yaml @@ -0,0 +1,52 @@ +name: Admin.Client.Upgrade.RedHat +description: | + Remotely push new client updates to Red Hat hosts. + + NOTE: This artifact requires that you supply a client Red Hat package by using the + tools interface or by using the "rpm client" command. Simply click on the tool + in the GUI and upload a package. + +tools: + - name: VelociraptorRedHat + +parameters: + - name: SleepDuration + default: "600" + type: int + description: | + The package is typically large and we do not want to + overwhelm the server so we stagger the download over this many + seconds. + + - name: ServiceName + default: "velociraptor_client" + type: str + description: | + The name of the service to restart after the upgrade. + +implied_permissions: + - EXECVE + +sources: + - precondition: + SELECT OS From info() where OS =~ 'linux' + + query: | + // FetchBinary downloads to /tmp on linux + LET bin <= SELECT OSPath AS Dest + FROM Artifact.Generic.Utils.FetchBinary( + ToolName="VelociraptorRedHat", IsExecutable=FALSE, + SleepDuration=SleepDuration) + + // Call the binary and return all its output in a single row. + // If we fail to download the binary we do not run the command. + SELECT * FROM foreach(row=bin, + query={ + SELECT * FROM chain( + // Install the new client (Disabled preun because older versions + // had a bug where preun would shut down the service - see #3122). + + b={SELECT * FROM execve(argv=["rpm", "--nopreun", "-U", str(str=Dest)])}, + c={SELECT * FROM execve(argv=["systemctl", "restart", ServiceName])} + ) + }) diff --git a/artifacts/definitions/Admin/Client/Upgrade/Windows.yaml b/artifacts/definitions/Admin/Client/Upgrade/Windows.yaml new file mode 100644 index 000000000..53f5cf3f1 --- /dev/null +++ b/artifacts/definitions/Admin/Client/Upgrade/Windows.yaml @@ -0,0 +1,65 @@ +name: Admin.Client.Upgrade.Windows +description: | + Remotely push new client updates. + + NOTE: This artifact requires that you supply a client MSI by using the + tools interface. Simply click on the tool in the GUI and upload a + pre-packaged MSI. + + While typically the MSI will contain the Velociraptor windows + client, you can install any other MSI as well by customizing this + artifact or uploading a different MSI file. + +tools: + - name: WindowsMSI + +parameters: + - name: SleepDuration + default: "600" + type: int + description: | + The MSI file is typically very large and we do not want to + overwhelm the server so we stagger the download over this many + seconds. + +implied_permissions: + - EXECVE + - FILESYSTEM_WRITE + +sources: + - precondition: + SELECT OS From info() where OS = 'windows' + + query: | + // Force the file to be copied to the real temp directory since + // we are just about to remove the Tools directory. + LET bin <= SELECT copy(filename=OSPath, + dest=expand(path="%SYSTEMROOT%\\Temp\\") + basename(path=OSPath)) AS Dest + FROM Artifact.Generic.Utils.FetchBinary( + ToolName="WindowsMSI", IsExecutable=FALSE, + SleepDuration=SleepDuration) + + // Call the binary and return all its output in a single row. + // If we fail to download the binary we do not run the command. + + // msiexec needs some random set of commands to really force a + // reinstall. We dont know which one will be correct at runtime so + // we just try them all. If we succeed then the client will get + // killed and restarted. + SELECT * FROM foreach(row=bin, + query={ + SELECT * FROM chain(a={ + SELECT * FROM execve( + argv=["msiexec.exe", "/i", Dest, "/q", "REINSTALL=ALL", "REINSTALLMODE=A"], + length=10000000) + + }, b={ + SELECT * FROM execve( + argv=["msiexec.exe", "/i", Dest, "/q"], length=10000000) + + }, c={ + SELECT * FROM execve( + argv=["msiexec.exe", "/f", "/i", Dest, "/q"], length=10000000) + + }) + }) diff --git a/artifacts/definitions/Admin/Events/PostProcessUploads.yaml b/artifacts/definitions/Admin/Events/PostProcessUploads.yaml deleted file mode 100644 index b9d8085a9..000000000 --- a/artifacts/definitions/Admin/Events/PostProcessUploads.yaml +++ /dev/null @@ -1,46 +0,0 @@ -name: Admin.Events.PostProcessUploads -description: | - Sometimes we would like to post process uploads collected as part of - the hunt's artifact collections - - Post processing means to watch the hunt for completed flows and run - a post processing command on the files obtained from each host. - - The command will receive the list of paths of the files uploaded by - the artifact. We dont actually care what the command does with those - files - we will just relay our stdout/stderr to the artifact's - result set. - -type: SERVER_EVENT - -required_permissions: - - EXECVE - -parameters: - - name: uploadPostProcessCommand - description: | - The command to run - must be a json array of strings! The list - of files will be appended to the end of the command. - default: | - ["/bin/ls", "-l"] - - - name: uploadPostProcessArtifact - description: | - The name of the artifact to watch. - default: Windows.Registry.NTUser.Upload - -sources: - - query: | - LET files = SELECT Flow, - array(a1=parse_json_array(data=uploadPostProcessCommand), - a2=file_store(path=Flow.uploaded_files)) as Argv - FROM watch_monitoring(artifact='System.Flow.Completion') - WHERE uploadPostProcessArtifact in Flow.artifacts_with_results - - SELECT * from foreach( - row=files, - query={ - SELECT Flow.session_id as FlowId, Argv, - Stdout, Stderr, ReturnCode - FROM execve(argv=Argv) - }) diff --git a/artifacts/definitions/Admin/System/CompressUploads.yaml b/artifacts/definitions/Admin/System/CompressUploads.yaml deleted file mode 100644 index cadb45830..000000000 --- a/artifacts/definitions/Admin/System/CompressUploads.yaml +++ /dev/null @@ -1,41 +0,0 @@ -name: Admin.System.CompressUploads -description: | - Compresses all uploaded files. - - When artifacts collect files they are normally stored on the server - uncompressed. This artifact watches all completed flows and - compresses the files in the file store when the flow completes. This - is very useful for cloud based deployments with limited storage - space or when collecting large files. - - In order to run this artifact you would normally run it as part of - an artifact acquisition process: - - ``` - $ velociraptor --config /etc/server.config.yaml artifacts acquire Admin.System.CompressUploads - ``` - - Note that there is nothing special about compressed files - you can - also just run `find` and `gzip` in the file store. Velociraptor will - automatically decompress the file when displaying it in the GUI - text/hexdump etc. - -type: SERVER_EVENT - -parameters: - - name: blacklistCompressionFilename - type: regex - description: Filenames which match this regex will be excluded from compression. - default: 'ntuser.dat$' - -sources: - - query: | - LET files = SELECT ClientId, - Flow.session_id as Flow, - Flow.uploaded_files as Files - FROM watch_monitoring(artifact='System.Flow.Completion') - WHERE Files and not Files =~ blacklistCompressionFilename - - SELECT ClientId, Flow, Files, - compress(path=Files) as CompressedFiles - FROM files diff --git a/artifacts/definitions/Demo/Plugins/Fifo.yaml b/artifacts/definitions/Demo/Plugins/Fifo.yaml index 1d974be6b..bf8a616cf 100644 --- a/artifacts/definitions/Demo/Plugins/Fifo.yaml +++ b/artifacts/definitions/Demo/Plugins/Fifo.yaml @@ -7,16 +7,16 @@ description: | You can use this to build queries which consider historical events together with current events at the same time. In this example, we - check for a successful logon preceded by a number of failed logon + check for a successful logon preceded by several failed logon attempts. In this example, we use the clock() plugin to simulate events. We - simulate failed logon attempts using the clock() plugin every + simulate failed logon attempts by using the clock() plugin every second. By feeding the failed logon events to the fifo() plugin we ensure the fifo() plugin cache contains the last 5 failed logon events. - We simulate a successful logon event every 3 seconds, again using + We simulate a successful logon event every 3 seconds, again by using the clock plugin. Once a successful logon event is detected, we go back over the last 5 login events, count them and collect the last failed logon times (using the GROUP BY operator we group the diff --git a/artifacts/definitions/Demo/Plugins/GUI.yaml b/artifacts/definitions/Demo/Plugins/GUI.yaml old mode 100644 new mode 100755 index 447aab0ae..933b849cc --- a/artifacts/definitions/Demo/Plugins/GUI.yaml +++ b/artifacts/definitions/Demo/Plugins/GUI.yaml @@ -12,6 +12,7 @@ resources: parameters: - name: ChoiceSelector + description: Choose one item from a selection type: choices default: First Choice choices: @@ -19,17 +20,35 @@ parameters: - Second Choice - Third Choice + - name: MultiChoiceSelector + description: Choose one or more items from a selection + type: multichoice + default: '["Bananas"]' + choices: + - Apples + - Bananas + - Oranges + - Grapes + + - name: Hashes + validating_regex: '^\s*([A-F0-9]+\s*)+$' + description: One or more hashes in hex separated by white space. + - name: RegularExpression type: regex default: "." + - name: MultipleRegularExpression + type: regex_array + default: '[".+"]' + - name: YaraRule type: yara - name: Flag friendly_name: A Flag with a name type: bool - default: Y + default: True - name: Flag2 type: bool @@ -58,41 +77,68 @@ parameters: A,B C,D - - name: CSVData2 - type: csv - default: | - Column1,Column2 - A,B - C,D - - name: JSONData type: json_array - default: "[]" + default: '["First","Second"]' - - name: JSONData2 + - name: JSONDataWithObject type: json_array default: | [{"foo": "bar"}] - name: FileUpload1 type: upload - description: FileUpload1 can receive a file upload. The upload content will be available in this variable when executing on the client. + description: | + FileUpload1 can receive a file upload. + + The upload content will be available in this variable when + executing on the client. + + - name: FileUpload2 + type: upload_file + description: | + FileUpload2 can receive a file upload. + + The upload content will be stored in a temp file which will be + available in this variable when executing on the client. + + - name: ArtifactSelections + type: artifactset + description: A selection of artifact + artifact_type: CLIENT_EVENT + default: | + Artifact + Windows.Detection.PsexecService + Windows.Events.ProcessCreation + Windows.Events.ServiceCreation column_types: - - name: Hex + - name: Base64Hex type: base64hex sources: - query: | - SELECT base64encode(string="This should popup in a hex editor") AS Hex, - ChoiceSelector, Flag, Flag2, Flag3, + SELECT base64encode(string="This should popup in a hex editor") AS Base64Hex, + ChoiceSelector, MultiChoiceSelector, Flag, Flag2, Flag3, OffFlag, StartDate, StartDate2, StartDate3, - CSVData, CSVData2, JSONData, JSONData2, - len(list=FileUpload1) AS FileUpload1Length + CSVData, JSONData, JSONDataWithObject, + len(list=FileUpload1) AS FileUpload1Length, + stat(filename=FileUpload2) AS FileUpload2Stats FROM scope() notebook: - - type: md + - type: vql_suggestion + name: Test Suggestion + template: | + /* + # This is a suggestion notebook cell. + + It should be available from the suggestions list. + */ + SELECT * FROM info() + + - type: markdown + name: Test Template template: | # GUI Notebook tests @@ -102,16 +148,22 @@ sources: **Each of the below cells should have a H2 heading** - ## Check that notebok environment variables are populated - {{ $x := Query "SELECT * FROM items(\ - item=dict(NotebookId=NotebookId, ClientId=ClientId,\ - FlowId=FlowId, ArtifactName=ArtifactName))" | Expand }} + ## Check that notebook environment variables are populated + + Some of these are populated from the artifact parameters. + + {{ $x := Query "LET X = scope() SELECT * FROM items(\ + item=dict(NotebookId=X.NotebookId, ClientId=X.ClientId,\ + FlowId=X.FlowId, ArtifactName=X.ArtifactName, \ + ChoiceSelector=X.ChoiceSelector, StartDate=X.StartDate, \ + HuntId=X.HuntId))" | Expand }} {{ range $x }} * {{ Get . "_key" }} - {{ Get . "_value" }} {{- end -}} - - type: md + - type: markdown + name: Test Code Highlighting template: | ## Code syntax highlighting for VQL @@ -120,6 +172,7 @@ sources: ``` - type: vql + name: Test Markdown in VQL cell template: | /* ## A VQL cell with a heading. @@ -156,10 +209,20 @@ sources: FlowId, ClientId, URL, URL AS SafeURL, Base64Data, - "Hello" AS Data + format(format="%02x", args="Hello") AS Data, + TRUE, 4, NULL FROM scope() + - type: VQL + name: Test Default ColumnTypes + template: | + /* + ## Ensure that Base64hex data is automatically typed + */ + SELECT base64encode(string="This should popup in a hex editor") AS Base64Hex FROM scope() + - type: Markdown + name: Scatter Chart template: | ## Scatter Chart with a named column @@ -218,6 +281,7 @@ sources: {{ Query "LineTest" | LineChart }} - type: Markdown + name: Line Chart template: | ## A Line Chart @@ -226,6 +290,7 @@ sources: {{ define "Q" }} SELECT _ts, CPUPercent FROM monitoring( + client_id="server", artifact="Server.Monitor.Health/Prometheus", start_time=now() - 10 * 60) LIMIT 100 @@ -234,14 +299,18 @@ sources: {{ Query "Q" | TimeChart }} - type: vql + name: Test Timeline template: | /* ## Adding timelines - Add a timeline from this time series data + Add a timeline from this time series data. (This only works + for root org because it relies on server health events). + */ SELECT timestamp(epoch=_ts) AS Timestamp, CPUPercent FROM monitoring( + client_id="server", source="Prometheus", artifact="Server.Monitor.Health", start_time=now() - 10 * 60) @@ -250,6 +319,7 @@ sources: timestamp(epoch=_ts) AS Timestamp, dict(X=CPUPercent, Y=1) AS Dict FROM monitoring( + client_id="server", source="Prometheus", artifact="Server.Monitor.Health", start_time=now() - 10 * 60) @@ -264,6 +334,7 @@ sources: FROM scope() - type: Markdown + name: Test Cell Environment env: - key: Timeline value: Test "Timeline 你好世界" @@ -276,13 +347,15 @@ sources: {{ Scope "Timeline" | Timeline }} - type: VQL + name: Test Table Scrolling template: | /* # Test table scrolling. Check both expanded and contracted states of the cell */ - LET Test = "Hellothereongline" + LET zalgo = "1̴̣̜̗̰͇͖͖̞̮͈̂͜Í.̸̢̧̨͙̻̜̰̼̔̿̓̄̀̅͌̈́͒͗̈́̒̕̚͜͠e̶̙̞̬̹̥͖̤̟͑͒̂̀̔͠x̵Ì̈́͂Í̛̱̠̳̎̽̇̀Í̦̘̤̙͚̙͈̬e̵͒̑̕̚̕͠ÌĮ̯̦̫͖͖̀͜Í͈̟̠͉̥" + LET Test = "Hellothereongline" + zalgo SELECT Test AS Test1, Test AS Test2, Test AS Test3, Test AS Test4, Test AS Test5, @@ -293,6 +366,7 @@ sources: FROM range(start=0, end=100, step=1) - type: VQL + name: Test Column Types template: | /* # Column types set in the artifact's `column_types` field @@ -300,9 +374,74 @@ sources: These apply to notebooks automatically without needing to define them again. + * Hash column should right click to VT + * upload preview should show the uploaded file. + */ - LET ColumnTypes = dict(`StartDate`='timestamp') + LET ColumnTypes = dict(`StartDate`='timestamp', Download='download', + Hex='hex', Upload='preview_upload') + LET Hex = "B0 EC 48 5F 18 77" - SELECT Hex, StartDate + SELECT Hex, StartDate, hash(accessor="data", path="Hello") AS Hash, + upload(accessor="data", file="Hello world", + name="test.txt") AS Upload, + upload(accessor="data", file="Hello world", + name="test.txt") AS Download FROM source() + + - type: VQL + name: Test JSON renderer + template: | + /* Test the JSON renderer. */ + LET Strings = SELECT "Hello World" AS A FROM range(end=100) + + LET MultiColumn = SELECT * FROM chain(a={ + SELECT 1 AS A FROM range(end=10) + }, b={ + SELECT 1 AS B FROM range(end=10) + }) + + SELECT dict( + MultiColumn=MultiColumn, + Strings=Strings.A, + `NULL`=NULL, + Bool=TRUE, + BoolF=FALSE, + BinaryData=base64encode(string="hello world"), + Rows={ + SELECT count() AS Count, + rand() AS R + FROM range(end=20) + }, + Integer=1, Float=1.235, + LongString="Hello world " * 100, + MixedList=[1, 2, dict(A=3)], + NestedDict=dict( + Foo=dict(A=1, + B=dict(z=1, + nesting=dict(Foo="Hello world"))))) AS A + FROM scope() + + - type: VQL + name: Test Links + template: | + /* + # Test the link_to() VQL Function + */ + LET ColumnTypes <= dict( + LinkToFlow="url_internal", + LinkToHunt="url_internal", + LinkToArtifact="url_internal", + Download="url_internal", + LinkToClient="url_internal") + + LET s = scope() + LET Uploaded <= upload(accessor="data", file="Hello", name="test.txt") + + SELECT link_to(client_id=ClientId, flow_id=s.FlowId || "F.123") AS LinkToFlow, + link_to(client_id=ClientId) AS LinkToClient, + link_to(hunt_id=s.HuntId || "H.123") AS LinkToHunt, + link_to(artifact=ArtifactName) AS LinkToArtifact, + link_to(upload=Uploaded) AS Download + FROM scope() diff --git a/artifacts/definitions/Elastic/EventLogs/Sysmon.yaml b/artifacts/definitions/Elastic/EventLogs/Sysmon.yaml new file mode 100644 index 000000000..455baf91f --- /dev/null +++ b/artifacts/definitions/Elastic/EventLogs/Sysmon.yaml @@ -0,0 +1,617 @@ +name: Elastic.EventLogs.Sysmon +description: | + Ships the the Sysmon event log in ECS schema. + + The Elastic Common Schema (ECS) is an open source specification, + developed with support from the Elastic user community. ECS defines + a common set of fields to be used when storing event data in + Elasticsearch, such as logs and metrics. + + NOTE: ECS is poorly documented. There is no clear documentation of + where each field in the ECS record comes from other than the actual + source code of the Winlogbeat client. This artifact implements the + Winlogbeat transformation as described in + https://github.com/elastic/beats/blob/master/x-pack/winlogbeat/module/sysmon/ingest/sysmon.yml + + There may be slight variations between the data produced by this + artifact and the official Winlogbeat client. If you find such + variation, please file an issue on Velociraptor's GitHub issue + board. + +reference: + - https://www.elastic.co/guide/en/ecs/current/ecs-reference.html + +parameters: + - name: LogFileGlob + default: C:/Windows/System32/WinEvt/Logs/Microsoft-Windows-Sysmon%4Operational.evtx + +export: | + -- ECS clears many fields from EventData but we preserve them all, + -- although to ensure that Elastic does not reject the fields we + -- convert them all to strings. + LET NormalizeEventData(EventData) = to_dict(item={ + SELECT _key, str(str=_value) AS _value FROM items(item=EventData) + }) + + LET OpcodesLookup <= dict( + `0`= "Info", + `1`= "Start", + `2`= "Stop", + `3`= "DCStart", + `4`= "DCStop", + `5`= "Extension", + `6`= "Reply", + `7`= "Resume", + `8`= "Suspend", + `9`= "Send") + + LET LevelLookup <= dict( + `0`= "Information", + `1`= "Critical", + `2`= "Error", + `3`= "Warning", + `4`= "Information", + `5`= "Verbose") + + LET CategoryLookup <= dict( + `1`=["process",], + `2`=["file",], + `3`=["network",], + `4`=["process",], + `5`=["process",], + `6`=["driver",], + `7`=["process",], + `8`=["process",], + `9`=["process",], + `10`=["process",], + `11`=["file",], + `12`=["configuration","registry"], + `13`=["configuration","registry"], + `14`=["configuration","registry"], + `15`=["file",], + `16`=["configuration",], + `17`=["file",], + `18`=["file",], + `19`=["process",], + `20`=["process",], + `21`=["network",], + `22`=["network",], + `23`=["file",], + `24`=["",], + `25`=["process",], + `26`=["file",], + `27`=["file",], + `28`=["file",], + `255`=["process",]) + + LET TypeLookup <= dict( + `1`=["start",], + `2`=["change",], + `3`=["start", "connection", "protocol"], + `4`=["change",], + `5`=["end",], + `6`=["start",], + `7`=["change",], + `8`=["change",], + `9`=["access",], + `10`=["access",], + `11`=["creation",], + `12`=["change",], + `13`=["change",], + `14`=["change",], + `15`=["access",], + `16`=["change",], + `17`=["creation",], + `18`=["access",], + `19`=["creation",], + `20`=["creation",], + `21`=["access",], + `22`=["connection", "protocol", "info"], + `23`=["deletion",], + `24`=["change",], + `25`=["change",], + `26`=["deletion",], + `27`=["creation", "denied"], + `28`=["deletion", "denied"], + `255`=["error",]) + + LET DNSLookup <= dict( + `1`= "A", + `2`= "NS", + `3`= "MD", + `4`= "MF", + `5`= "CNAME", + `6`= "SOA", + `7`= "MB", + `8`= "MG", + `9`= "MR", + `10`= "NULL", + `11`= "WKS", + `12`= "PTR", + `13`= "HINFO", + `14`= "MINFO", + `15`= "MX", + `16`= "TXT", + `17`= "RP", + `18`= "AFSDB", + `19`= "X25", + `20`= "ISDN", + `21`= "RT", + `22`= "NSAP", + `23`= "NSAPPTR", + `24`= "SIG", + `25`= "KEY", + `26`= "PX", + `27`= "GPOS", + `28`= "AAAA", + `29`= "LOC", + `30`= "NXT", + `31`= "EID", + `32`= "NIMLOC", + `33`= "SRV", + `34`= "ATMA", + `35`= "NAPTR", + `36`= "KX", + `37`= "CERT", + `38`= "A6", + `39`= "DNAME", + `40`= "SINK", + `41`= "OPT", + `43`= "DS", + `46`= "RRSIG", + `47`= "NSEC", + `48`= "DNSKEY", + `49`= "DHCID", + `100`= "UINFO", + `101`= "UID", + `102`= "GID", + `103`= "UNSPEC", + `248`= "ADDRS", + `249`= "TKEY", + `250`= "TSIG", + `251`= "IXFR", + `252`= "AXFR", + `253`= "MAILB", + `254`= "MAILA", + `255`= "ANY", + `65281`= "WINS", + `65282`= "WINSR" + ) + + LET DnsStatusLookup <= dict( + `5`= "ERROR_ACCESS_DENIED", + `0`= "SUCCESS", + `8`= "ERROR_NOT_ENOUGH_MEMORY", + `13`= "ERROR_INVALID_DATA", + `14`= "ERROR_OUTOFMEMORY", + `123`= "ERROR_INVALID_NAME", + `1214`= "ERROR_INVALID_NETNAME", + `1223`= "ERROR_CANCELLED", + `1460`= "ERROR_TIMEOUT", + `4312`= "ERROR_OBJECT_NOT_FOUND", + `9001`= "DNS_ERROR_RCODE_FORMAT_ERROR", + `9002`= "DNS_ERROR_RCODE_SERVER_FAILURE", + `9003`= "DNS_ERROR_RCODE_NAME_ERROR", + `9004`= "DNS_ERROR_RCODE_NOT_IMPLEMENTED", + `9005`= "DNS_ERROR_RCODE_REFUSED", + `9006`= "DNS_ERROR_RCODE_YXDOMAIN", + `9007`= "DNS_ERROR_RCODE_YXRRSET", + `9008`= "DNS_ERROR_RCODE_NXRRSET", + `9009`= "DNS_ERROR_RCODE_NOTAUTH", + `9010`= "DNS_ERROR_RCODE_NOTZONE", + `9016`= "DNS_ERROR_RCODE_BADSIG", + `9017`= "DNS_ERROR_RCODE_BADKEY", + `9018`= "DNS_ERROR_RCODE_BADTIME", + `9101`= "DNS_ERROR_KEYMASTER_REQUIRED", + `9102`= "DNS_ERROR_NOT_ALLOWED_ON_SIGNED_ZONE", + `9103`= "DNS_ERROR_NSEC3_INCOMPATIBLE_WITH_RSA_SHA1", + `9104`= "DNS_ERROR_NOT_ENOUGH_SIGNING_KEY_DESCRIPTORS", + `9105`= "DNS_ERROR_UNSUPPORTED_ALGORITHM", + `9106`= "DNS_ERROR_INVALID_KEY_SIZE", + `9107`= "DNS_ERROR_SIGNING_KEY_NOT_ACCESSIBLE", + `9108`= "DNS_ERROR_KSP_DOES_NOT_SUPPORT_PROTECTION", + `9109`= "DNS_ERROR_UNEXPECTED_DATA_PROTECTION_ERROR", + `9110`= "DNS_ERROR_UNEXPECTED_CNG_ERROR", + `9111`= "DNS_ERROR_UNKNOWN_SIGNING_PARAMETER_VERSION", + `9112`= "DNS_ERROR_KSP_NOT_ACCESSIBLE", + `9113`= "DNS_ERROR_TOO_MANY_SKDS", + `9114`= "DNS_ERROR_INVALID_ROLLOVER_PERIOD", + `9115`= "DNS_ERROR_INVALID_INITIAL_ROLLOVER_OFFSET", + `9116`= "DNS_ERROR_ROLLOVER_IN_PROGRESS", + `9117`= "DNS_ERROR_STANDBY_KEY_NOT_PRESENT", + `9118`= "DNS_ERROR_NOT_ALLOWED_ON_ZSK", + `9119`= "DNS_ERROR_NOT_ALLOWED_ON_ACTIVE_SKD", + `9120`= "DNS_ERROR_ROLLOVER_ALREADY_QUEUED", + `9121`= "DNS_ERROR_NOT_ALLOWED_ON_UNSIGNED_ZONE", + `9122`= "DNS_ERROR_BAD_KEYMASTER", + `9123`= "DNS_ERROR_INVALID_SIGNATURE_VALIDITY_PERIOD", + `9124`= "DNS_ERROR_INVALID_NSEC3_ITERATION_COUNT", + `9125`= "DNS_ERROR_DNSSEC_IS_DISABLED", + `9126`= "DNS_ERROR_INVALID_XML", + `9127`= "DNS_ERROR_NO_VALID_TRUST_ANCHORS", + `9128`= "DNS_ERROR_ROLLOVER_NOT_POKEABLE", + `9129`= "DNS_ERROR_NSEC3_NAME_COLLISION", + `9130`= "DNS_ERROR_NSEC_INCOMPATIBLE_WITH_NSEC3_RSA_SHA1", + `9501`= "DNS_INFO_NO_RECORDS", + `9502`= "DNS_ERROR_BAD_PACKET", + `9503`= "DNS_ERROR_NO_PACKET", + `9504`= "DNS_ERROR_RCODE", + `9505`= "DNS_ERROR_UNSECURE_PACKET", + `9506`= "DNS_REQUEST_PENDING", + `9551`= "DNS_ERROR_INVALID_TYPE", + `9552`= "DNS_ERROR_INVALID_IP_ADDRESS", + `9553`= "DNS_ERROR_INVALID_PROPERTY", + `9554`= "DNS_ERROR_TRY_AGAIN_LATER", + `9555`= "DNS_ERROR_NOT_UNIQUE", + `9556`= "DNS_ERROR_NON_RFC_NAME", + `9557`= "DNS_STATUS_FQDN", + `9558`= "DNS_STATUS_DOTTED_NAME", + `9559`= "DNS_STATUS_SINGLE_PART_NAME", + `9560`= "DNS_ERROR_INVALID_NAME_CHAR", + `9561`= "DNS_ERROR_NUMERIC_NAME", + `9562`= "DNS_ERROR_NOT_ALLOWED_ON_ROOT_SERVER", + `9563`= "DNS_ERROR_NOT_ALLOWED_UNDER_DELEGATION", + `9564`= "DNS_ERROR_CANNOT_FIND_ROOT_HINTS", + `9565`= "DNS_ERROR_INCONSISTENT_ROOT_HINTS", + `9566`= "DNS_ERROR_DWORD_VALUE_TOO_SMALL", + `9567`= "DNS_ERROR_DWORD_VALUE_TOO_LARGE", + `9568`= "DNS_ERROR_BACKGROUND_LOADING", + `9569`= "DNS_ERROR_NOT_ALLOWED_ON_RODC", + `9570`= "DNS_ERROR_NOT_ALLOWED_UNDER_DNAME", + `9571`= "DNS_ERROR_DELEGATION_REQUIRED", + `9572`= "DNS_ERROR_INVALID_POLICY_TABLE", + `9573`= "DNS_ERROR_ADDRESS_REQUIRED", + `9601`= "DNS_ERROR_ZONE_DOES_NOT_EXIST", + `9602`= "DNS_ERROR_NO_ZONE_INFO", + `9603`= "DNS_ERROR_INVALID_ZONE_OPERATION", + `9604`= "DNS_ERROR_ZONE_CONFIGURATION_ERROR", + `9605`= "DNS_ERROR_ZONE_HAS_NO_SOA_RECORD", + `9606`= "DNS_ERROR_ZONE_HAS_NO_NS_RECORDS", + `9607`= "DNS_ERROR_ZONE_LOCKED", + `9608`= "DNS_ERROR_ZONE_CREATION_FAILED", + `9609`= "DNS_ERROR_ZONE_ALREADY_EXISTS", + `9610`= "DNS_ERROR_AUTOZONE_ALREADY_EXISTS", + `9611`= "DNS_ERROR_INVALID_ZONE_TYPE", + `9612`= "DNS_ERROR_SECONDARY_REQUIRES_MASTER_IP", + `9613`= "DNS_ERROR_ZONE_NOT_SECONDARY", + `9614`= "DNS_ERROR_NEED_SECONDARY_ADDRESSES", + `9615`= "DNS_ERROR_WINS_INIT_FAILED", + `9616`= "DNS_ERROR_NEED_WINS_SERVERS", + `9617`= "DNS_ERROR_NBSTAT_INIT_FAILED", + `9618`= "DNS_ERROR_SOA_DELETE_INVALID", + `9619`= "DNS_ERROR_FORWARDER_ALREADY_EXISTS", + `9620`= "DNS_ERROR_ZONE_REQUIRES_MASTER_IP", + `9621`= "DNS_ERROR_ZONE_IS_SHUTDOWN", + `9622`= "DNS_ERROR_ZONE_LOCKED_FOR_SIGNING", + `9651`= "DNS_ERROR_PRIMARY_REQUIRES_DATAFILE", + `9652`= "DNS_ERROR_INVALID_DATAFILE_NAME", + `9653`= "DNS_ERROR_DATAFILE_OPEN_FAILURE", + `9654`= "DNS_ERROR_FILE_WRITEBACK_FAILED", + `9655`= "DNS_ERROR_DATAFILE_PARSING", + `9701`= "DNS_ERROR_RECORD_DOES_NOT_EXIST", + `9702`= "DNS_ERROR_RECORD_FORMAT", + `9703`= "DNS_ERROR_NODE_CREATION_FAILED", + `9704`= "DNS_ERROR_UNKNOWN_RECORD_TYPE", + `9705`= "DNS_ERROR_RECORD_TIMED_OUT", + `9706`= "DNS_ERROR_NAME_NOT_IN_ZONE", + `9707`= "DNS_ERROR_CNAME_LOOP", + `9708`= "DNS_ERROR_NODE_IS_CNAME", + `9709`= "DNS_ERROR_CNAME_COLLISION", + `9710`= "DNS_ERROR_RECORD_ONLY_AT_ZONE_ROOT", + `9711`= "DNS_ERROR_RECORD_ALREADY_EXISTS", + `9712`= "DNS_ERROR_SECONDARY_DATA", + `9713`= "DNS_ERROR_NO_CREATE_CACHE_DATA", + `9714`= "DNS_ERROR_NAME_DOES_NOT_EXIST", + `9715`= "DNS_WARNING_PTR_CREATE_FAILED", + `9716`= "DNS_WARNING_DOMAIN_UNDELETED", + `9717`= "DNS_ERROR_DS_UNAVAILABLE", + `9718`= "DNS_ERROR_DS_ZONE_ALREADY_EXISTS", + `9719`= "DNS_ERROR_NO_BOOTFILE_IF_DS_ZONE", + `9720`= "DNS_ERROR_NODE_IS_DNAME", + `9721`= "DNS_ERROR_DNAME_COLLISION", + `9722`= "DNS_ERROR_ALIAS_LOOP", + `9751`= "DNS_INFO_AXFR_COMPLETE", + `9752`= "DNS_ERROR_AXFR", + `9753`= "DNS_INFO_ADDED_LOCAL_WINS", + `9801`= "DNS_STATUS_CONTINUE_NEEDED", + `9851`= "DNS_ERROR_NO_TCPIP", + `9852`= "DNS_ERROR_NO_DNS_SERVERS", + `9901`= "DNS_ERROR_DP_DOES_NOT_EXIST", + `9902`= "DNS_ERROR_DP_ALREADY_EXISTS", + `9903`= "DNS_ERROR_DP_NOT_ENLISTED", + `9904`= "DNS_ERROR_DP_ALREADY_ENLISTED", + `9905`= "DNS_ERROR_DP_NOT_AVAILABLE", + `9906`= "DNS_ERROR_DP_FSMO_ERROR", + `9911`= "DNS_ERROR_RRL_NOT_ENABLED", + `9912`= "DNS_ERROR_RRL_INVALID_WINDOW_SIZE", + `9913`= "DNS_ERROR_RRL_INVALID_IPV4_PREFIX", + `9914`= "DNS_ERROR_RRL_INVALID_IPV6_PREFIX", + `9915`= "DNS_ERROR_RRL_INVALID_TC_RATE", + `9916`= "DNS_ERROR_RRL_INVALID_LEAK_RATE", + `9917`= "DNS_ERROR_RRL_LEAK_RATE_LESSTHAN_TC_RATE", + `9921`= "DNS_ERROR_VIRTUALIZATION_INSTANCE_ALREADY_EXISTS", + `9922`= "DNS_ERROR_VIRTUALIZATION_INSTANCE_DOES_NOT_EXIST", + `9923`= "DNS_ERROR_VIRTUALIZATION_TREE_LOCKED", + `9924`= "DNS_ERROR_INVAILD_VIRTUALIZATION_INSTANCE_NAME", + `9925`= "DNS_ERROR_DEFAULT_VIRTUALIZATION_INSTANCE", + `9951`= "DNS_ERROR_ZONESCOPE_ALREADY_EXISTS", + `9952`= "DNS_ERROR_ZONESCOPE_DOES_NOT_EXIST", + `9953`= "DNS_ERROR_DEFAULT_ZONESCOPE", + `9954`= "DNS_ERROR_INVALID_ZONESCOPE_NAME", + `9955`= "DNS_ERROR_NOT_ALLOWED_WITH_ZONESCOPES", + `9956`= "DNS_ERROR_LOAD_ZONESCOPE_FAILED", + `9957`= "DNS_ERROR_ZONESCOPE_FILE_WRITEBACK_FAILED", + `9958`= "DNS_ERROR_INVALID_SCOPE_NAME", + `9959`= "DNS_ERROR_SCOPE_DOES_NOT_EXIST", + `9960`= "DNS_ERROR_DEFAULT_SCOPE", + `9961`= "DNS_ERROR_INVALID_SCOPE_OPERATION", + `9962`= "DNS_ERROR_SCOPE_LOCKED", + `9963`= "DNS_ERROR_SCOPE_ALREADY_EXISTS", + `9971`= "DNS_ERROR_POLICY_ALREADY_EXISTS", + `9972`= "DNS_ERROR_POLICY_DOES_NOT_EXIST", + `9973`= "DNS_ERROR_POLICY_INVALID_CRITERIA", + `9974`= "DNS_ERROR_POLICY_INVALID_SETTINGS", + `9975`= "DNS_ERROR_CLIENT_SUBNET_IS_ACCESSED", + `9976`= "DNS_ERROR_CLIENT_SUBNET_DOES_NOT_EXIST", + `9977`= "DNS_ERROR_CLIENT_SUBNET_ALREADY_EXISTS", + `9978`= "DNS_ERROR_SUBNET_DOES_NOT_EXIST", + `9979`= "DNS_ERROR_SUBNET_ALREADY_EXISTS", + `9980`= "DNS_ERROR_POLICY_LOCKED", + `9981`= "DNS_ERROR_POLICY_INVALID_WEIGHT", + `9982`= "DNS_ERROR_POLICY_INVALID_NAME", + `9983`= "DNS_ERROR_POLICY_MISSING_CRITERIA", + `9984`= "DNS_ERROR_INVALID_CLIENT_SUBNET_NAME", + `9985`= "DNS_ERROR_POLICY_PROCESSING_ORDER_INVALID", + `9986`= "DNS_ERROR_POLICY_SCOPE_MISSING", + `9987`= "DNS_ERROR_POLICY_SCOPE_NOT_ALLOWED", + `9988`= "DNS_ERROR_SERVERSCOPE_IS_REFERENCED", + `9989`= "DNS_ERROR_ZONESCOPE_IS_REFERENCED", + `9990`= "DNS_ERROR_POLICY_INVALID_CRITERIA_CLIENT_SUBNET", + `9991`= "DNS_ERROR_POLICY_INVALID_CRITERIA_TRANSPORT_PROTOCOL", + `9992`= "DNS_ERROR_POLICY_INVALID_CRITERIA_NETWORK_PROTOCOL", + `9993`= "DNS_ERROR_POLICY_INVALID_CRITERIA_INTERFACE", + `9994`= "DNS_ERROR_POLICY_INVALID_CRITERIA_FQDN", + `9995`= "DNS_ERROR_POLICY_INVALID_CRITERIA_QUERY_TYPE", + `9996`= "DNS_ERROR_POLICY_INVALID_CRITERIA_TIME_OF_DAY", + `10054`= "WSAECONNRESET", + `10055`= "WSAENOBUFS", + `10060`= "WSAETIMEDOUT" + ) + + LET ParseDNSAnswers(X) = SELECT if(condition=_value =~ "^type", + then=dict( + data=parse_string_with_regex( + string=regex_replace(source=_value, replace="", re="::ffff:"), + regex="(?P[^\\s]+)$").Data, + type=get(item=DNSLookup, + field=parse_string_with_regex( + string=_value, regex="type:\\s+([0-9]+)").g1)), + else=dict( + data=regex_replace(source=_value, replace="", re="::ffff:"), + type=if(condition=regex_replace(source=_value, replace="", re="::ffff:") =~ ":", + then="AAAA", else="A") + )) AS Field + FROM foreach(row=split(string=X, sep=";")) + WHERE _value + + LET ParseHashes(Hashes) = to_dict(item={ + SELECT split(string=_value, sep="=")[0] AS _key, + split(string=_value, sep="=")[1] AS _value + FROM foreach(row=split(string=Hashes, sep=",")) + }) + + LET _EventToECSBase(System, EventData) = dict( + ecs=dict(version="1.12.0"), + log=dict(level=System.Level), + event=dict( + module="sysmon", + kind="event", + code=System.EventID.Value, + category=get(item=CategoryLookup, field=str(str=System.EventID.Value)), + type=get(item=TypeLookup, field=str(str=System.EventID.Value)), + created=timestamp(epoch=System.TimeCreated.SystemTime) + ), + error=dict( + code=if(condition=System.EventID.Value = 255, then=EventData.ID, else=0) + ), + rule=dict( + name=EventData.RuleName + ), + message=if(condition=System.EventID.Value = 255, then=EventData.Type, else=""), + winlog=dict( + api="wineventlog", + channel=System.Channel, + computer_name=System.Computer, + event_data=NormalizeEventData(EventData=EventData), + event_id=System.EventID.Value , + opcode=get(item=OpcodesLookup, field=str(str=System.Opcode)), + process=dict( + pid=System.Execution.ProcessID, + thread=dict( + id=System.Execution.ThreadID + ) + ), + provider_guid=System.Provider.Guid, + provider_name=System.Provider.Name, + record_id=str(str=System.EventRecordID), + user=dict( + identifier=System.Security.UserID + ) + ) + ) + + LET _EventToECSProcess(System, EventData) = dict( + process=dict( + hash=ParseHashes(Hashes=EventData.Hashes), + entity_id=EventData.ProcessGuid || EventData.SourceProcessGuid || EventData.SourceProcessGUID, + pid=EventData.ProcessId || EventData.SourceProcessId, + executable=EventData.Image || EventData.SourceImage || EventData.Destination, + command_line=EventData.CommandLine, + working_directory=EventData.CurrentDirectory, + parent=dict( + pid=EventData.ParentProcessId, + entity_id= EventData.ParentProcessGuid, + executable=EventData.ParentImage, + command_line=EventData.ParentCommandLine, + args=commandline_split(command=EventData.ParentCommandLine), + args_count=len(list=commandline_split(command=EventData.ParentCommandLine)), + name=pathspec(parse=EventData.ParentImage, path_type="windows").Basename + ), + thread=dict( + id= EventData.SourceThreadId || 0 + ), + pe=if(condition=System.EventID.Value != 7, then=dict( + original_file_name=EventData.OriginalFileName || "", + company=EventData.Company || "", + description=EventData.Description || "", + file_version=EventData.FileVersion || "", + product= EventData.Product || "" + )), + args=commandline_split(command=EventData.CommandLine), + args_count=len(list=commandline_split(command=EventData.CommandLine)), + name=pathspec(parse=EventData.Image, path_type="windows").Basename + ) + ) + + LET _EventToECSNetwork(System, EventData) = dict( + network=dict( + transport=EventData.Protocol, + protocol=if(condition=System.EventID.Value = 22, then="dns", else=EventData.DestinationPortName || EventData.SourcePortName), + direction=if(condition= EventData.Initiated, then="egress", else="ingress"), + type=if(condition= EventData.SourceIsIpv6, then="ipv6", else="ipv4") + ), + source=dict( + ip=EventData.SourceIp, + domain=EventData.SourceHostname, + port=EventData.SourcePort + ), + destination=dict( + ip=EventData.DestinationIp, + domain=EventData.DestinationHostname, + port=EventData.DestinationPort + ), + dns=dict( + answers=ParseDNSAnswers(X=EventData.QueryResults).Field, + question=dict( + name=EventData.QueryName + ), + status=get(item=DnsStatusLookup, field=str(str=EventData.QueryStatus)) + ) + ) + + LET _ParseRegData(X) = if(condition=X =~ "^DWORD", + then=dict( + strings=[str(str=int(int= parse_string_with_regex(string=X, regex="\\((.+?)\\)").g1)),], + type="DWORD"), + else=if(condition=X =~ "^Binary Data", + then=dict( + strings=["Binary Data",], + type="REG_BINARY"), + else=if(condition=X =~ "^QWORD", + then=dict( + strings=[str(str=int(int= regex_replace(re="-0x", replace="", + source=parse_string_with_regex(string=X, regex="\\((.+?)\\)").g1))),], + type="QWORD"), + else=dict(strings=X, type=parse_string_with_regex(string=X, regex="(^[^\\S]+)").g1) + ) + )) + + LET _EventToECSRegistry(System, EventData) = dict( + process=dict( + entity_id=EventData.ProcessGuid || EventData.SourceProcessGuid || EventData.SourceProcessGUID, + pid=EventData.ProcessId || EventData.SourceProcessId, + executable=EventData.Image || EventData.SourceImage || EventData.Destination, + name=pathspec(parse=EventData.Image, path_type="windows").Basename + ), + registry=dict( + hive=pathspec(parse=EventData.TargetObject, path_type="registry")[0], + key=pathspec(parse=EventData.TargetObject, path_type="registry")[1:], + path=EventData.TargetObject, + value=pathspec(parse=EventData.TargetObject, path_type="registry").Basename, + data= _ParseRegData(X=EventData.Details) + ) + ) + + LET _EventToECSFile(System, EventData) = dict( + file=dict( + path=EventData.TargetFilename || EventData.Device || EventData.ImageLoaded, + directory=pathspec(parse=EventData.TargetFilename || EventData.Device || EventData.ImageLoaded, path_type="windows").Dirname, + name=EventData.PipeName || pathspec(parse=EventData.TargetFilename || EventData.Device || EventData.ImageLoaded, path_type="windows").Basename, + code_signature=dict( + subject_name= EventData.Signature || "", + status = EventData.SignatureStatus || "", + signed=if(condition=EventData.Signed, then=TRUE, else=FALSE), + valid=EventData.SignatureStatus = "Valid" + ), + process=dict( + entity_id=EventData.ProcessGuid || EventData.SourceProcessGuid || EventData.SourceProcessGUID, + pid=EventData.ProcessId || EventData.SourceProcessId, + executable=EventData.Image || EventData.SourceImage || EventData.Destination, + name=pathspec(parse=EventData.Image, path_type="windows").Basename, + hash=ParseHashes(Hashes=EventData.Hash) + ), + pe=dict( + original_file_name=EventData.OriginalFileName || "", + company=EventData.Company || "", + description=EventData.Description || "", + file_version=EventData.FileVersion || "", + product=EventData.Product || "" + ), + sysmon=dict( + file=dict( + archived=if(condition=EventData.Archived =~ "true", then=TRUE, else=FALSE), + is_executable=if(condition=EventData.is_executable, then=TRUE, else=FALSE) + ) + ) + ) + ) + + LET SysmonEventToECS(System, EventData) = _EventToECSBase(System=System, EventData=EventData) + if( + condition=get(item=CategoryLookup, field=str(str=System.EventID.Value)) =~ "process", + then=_EventToECSProcess(System=System, EventData=EventData), + else=if( + condition=get(item=CategoryLookup, field=str(str=System.EventID.Value)) =~ "network", + then=_EventToECSNetwork(System=System, EventData=EventData), + else=if( + condition=get(item=CategoryLookup, field=str(str=System.EventID.Value)) =~ "registry", + then=_EventToECSRegistry(System=System, EventData=EventData), + else=if( + condition=get(item=CategoryLookup, field=str(str=System.EventID.Value)) =~ "file", + then=_EventToECSFile(System=System, EventData=EventData), + else=dict())))) + +sources: + - query: | + SELECT * FROM foreach(row={ + SELECT * FROM foreach(row={ + SELECT OSPath FROM glob(globs=LogFileGlob) + }, query={ + SELECT SysmonEventToECS(System=System, EventData=EventData) AS ECS + FROM parse_evtx(filename=OSPath) + }) + }, column="ECS") + + notebook: + - type: vql_suggestion + name: "Upload to Elastic" + template: | + /* + * Modify the Elastic parameters to upload this dataset. + * You might need to add authentication to Elastic. + */ + LET ElasicAddress = "http://localhost:9200" + + // Uncomment this when you are ready to upload the data + LET X = SELECT * + FROM elastic_upload( + addresses=ElasicAddress, + index="winlogbeat-velo", + action="create", + query={ + SELECT timestamp(epoch=now()) AS `@timestamp`, + ClientId, + client_info(client_id=ClientId).Hostname AS Hostname, + * + FROM source(artifact="Elastic.EventLogs.Sysmon") + LIMIT 10 + }) diff --git a/artifacts/definitions/Elastic/Events/Clients.yaml b/artifacts/definitions/Elastic/Events/Clients.yaml deleted file mode 100644 index 2cd314b88..000000000 --- a/artifacts/definitions/Elastic/Events/Clients.yaml +++ /dev/null @@ -1,56 +0,0 @@ -name: Elastic.Events.Clients -description: | - This server monitoring artifact will watch a selection of client - monitoring artifacts for new events and push those to an elastic - index. - - NOTE: You must ensure you are collecting these artifacts from the - clients by adding them to the "Client Events" GUI. - -type: SERVER_EVENT - -parameters: - - name: WindowsDetectionPsexecService - description: Upload Windows.Detection.PsexecService to Elastic - type: bool - - name: WindowsEventsDNSQueries - description: Upload Windows.Events.DNSQueries to Elastic - type: bool - - name: WindowsEventsProcessCreation - description: Upload Windows.Events.ProcessCreation to Elastic - type: bool - - name: WindowsEventsServiceCreation - description: Upload Windows.Events.ServiceCreation to Elastic - type: bool - - name: ElasticAddresses - default: http://127.0.0.1:9200/ - - name: artifactParameterMap - type: hidden - default: | - Artifact,Parameter - Windows.Detection.PsexecService,WindowsDetectionPsexecService - Windows.Events.DNSQueries,WindowsEventsDNSQueries - Windows.Events.ProcessCreation,WindowsEventsProcessCreation - Windows.Events.ServiceCreation,WindowsEventsServiceCreation - -sources: - - query: | - LET artifacts_to_watch = SELECT Artifact FROM parse_csv( - filename=artifactParameterMap, accessor='data') - WHERE get(item=scope(), member=Parameter) AND log( - message="Uploading artifact " + Artifact + " to Elastic") - - LET events = SELECT * FROM foreach( - row=artifacts_to_watch, - async=TRUE, // Required for event queries in foreach() - query={ - SELECT *, "Artifact_" + Artifact as _index, - Artifact, - timestamp(epoch=now()) AS timestamp - FROM watch_monitoring(artifact=Artifact) - }) - - SELECT * FROM elastic_upload( - query=events, - type="ClientEvents", - addresses=split(string=ElasticAddresses, sep=",")) diff --git a/artifacts/definitions/Elastic/Events/Upload.yaml b/artifacts/definitions/Elastic/Events/Upload.yaml new file mode 100644 index 000000000..c3ae6e7de --- /dev/null +++ b/artifacts/definitions/Elastic/Events/Upload.yaml @@ -0,0 +1,85 @@ +name: Elastic.Events.Upload +aliases: +- Elastic.Events.Clients + +description: | + This server monitoring artifact will watch a selection of client or + server monitoring artifacts for new events and push those to an + elastic index. + + NOTE: You must ensure you are collecting these artifacts from the + clients by adding them to the "Client Events" GUI, or for server + artifacts, the "Server Events" GUI. + +type: SERVER_EVENT + +parameters: + - name: ElasticAddresses + default: http://127.0.0.1:9200/ + - name: Username + - name: Password + - name: APIKey + - name: ClientArtifactsToWatch + type: artifactset + artifact_type: CLIENT_EVENT + default: | + Artifact + Windows.Detection.PsexecService + Windows.Events.ProcessCreation + Windows.Events.ServiceCreation + - name: ServerArtifactsToWatch + type: artifactset + artifact_type: SERVER_EVENT + default: | + Artifact + Server.Audit.Logs + - name: DisableSSLSecurity + type: bool + description: Disable SSL certificate verification + - name: Threads + type: int + description: Number of threads to upload with + - name: ChunkSize + type: int + description: Batch this many rows for each upload. + - name: CloudID + description: The cloud id if needed + - name: RootCA + description: | + A root CA certificate in PEM for trusting TLS protected Elastic + servers. + +sources: + - query: | + LET artifacts_to_watch = SELECT * FROM chain( + a={SELECT Artifact FROM ClientArtifactsToWatch}, + b={SELECT Artifact FROM ServerArtifactsToWatch}) + WHERE NOT Artifact =~ "Elastic.Events.Upload" + AND log(message="Uploading artifact " + Artifact + " to Elastic") + + LET s = scope() + + LET events = SELECT * FROM foreach( + row=artifacts_to_watch, + async=TRUE, // Required for event queries in foreach() + query={ + SELECT *, "Artifact_" + Artifact as _index, + Artifact, + client_info(client_id=s.ClientId || "server").os_info.hostname AS Hostname, + timestamp(epoch=now()) AS timestamp + FROM watch_monitoring(artifact=Artifact) + }) + + SELECT * FROM elastic_upload( + query=events, + threads=Threads, + chunk_size=ChunkSize, + addresses=split(string=ElasticAddresses, sep=","), + index="velociraptor", + password=Password, + username=Username, + cloud_id=CloudID, + api_key=APIKey, + root_ca=RootCA, + disable_ssl_security=DisableSSLSecurity, + type="ClientEvents") diff --git a/artifacts/definitions/Elastic/Flows/Upload.yaml b/artifacts/definitions/Elastic/Flows/Upload.yaml index 500b92c24..d1b4c293d 100644 --- a/artifacts/definitions/Elastic/Flows/Upload.yaml +++ b/artifacts/definitions/Elastic/Flows/Upload.yaml @@ -8,6 +8,21 @@ description: | to adjust the index size/lifetime according to the artifact it is holding. + NOTE: Elastic is a database and still must have a stable + schema. This means that artifacts that produce inconsistent columns + and types will **NOT** work as expected. What will happen is that + the first row that is inserted will create the Elastic database + schema (In Elastic terminology "mapping") and then any subsequent + row with a different type for these fields will be rejected by + Elastic. + + In particular this does not work with event logs because event logs + have a varied schema (The EventData field is a free form field + depending on the event log itself). Therefore forwarding event log + data to Elastic with this artifact will cause Elastic to drop many + events!! This artifact is not suitable for forwarding Windows Event + Logs! + type: SERVER_EVENT parameters: @@ -20,12 +35,28 @@ parameters: - name: Username - name: Password - name: APIKey + - name: DisableSSLSecurity + type: bool + description: Disable SSL certificate verification + - name: Threads + type: int + description: Number of threads to upload with + - name: ChunkSize + type: int + description: Batch this many rows for each upload. + - name: CloudID + description: The cloud id if needed + - name: RootCA + description: | + A root CA certificate in PEM for trusting TLS protected Elastic + servers. sources: - query: | LET completions = SELECT * FROM watch_monitoring( artifact="System.Flow.Completion") WHERE Flow.artifacts_with_results =~ ArtifactNameRegex + LET organization <= org().name LET documents = SELECT * FROM foreach(row=completions, query={ @@ -33,10 +64,12 @@ sources: row=Flow.artifacts_with_results, query={ SELECT *, _value AS Artifact, + client_info(client_id=ClientId).os_info.hostname AS Hostname, timestamp(epoch=now()) AS timestamp, ClientId, Flow.session_id AS FlowId, "artifact_" + regex_replace(source=_value, - re='[/.]', replace='_') as _index + re='[/.]', replace='_') as _index, + organization as Organization FROM source( client_id=ClientId, flow_id=Flow.session_id, @@ -46,9 +79,14 @@ sources: SELECT * FROM elastic_upload( query=documents, + threads=Threads, + chunk_size=ChunkSize, addresses=split(string=elasticAddresses, sep=","), index="velociraptor", password=Password, username=Username, + cloud_id=CloudID, api_key=APIKey, + root_ca=RootCA, + disable_ssl_security=DisableSSLSecurity, type="artifact") diff --git a/artifacts/definitions/Generic/Applications/Chrome/SessionStorage.yaml b/artifacts/definitions/Generic/Applications/Chrome/SessionStorage.yaml new file mode 100644 index 000000000..d266622d2 --- /dev/null +++ b/artifacts/definitions/Generic/Applications/Chrome/SessionStorage.yaml @@ -0,0 +1,70 @@ +name: Generic.Applications.Chrome.SessionStorage +description: | + Session storage allows a web site to store permanent data in the + user's browser. + + This artifact parses this data from the browser cache. Each website + has maintains a mapping between keys and values. The data is stored + per website and can vary. + +parameters: +- name: SessionGlobs + type: csv + default: | + Glob + C:/Users/*/AppData/Local/Google/Chrome/User Data/*/Session Storage + C:/Users/*/AppData/Local/BraveSoftware/Brave*/User Data/*/Session Storage + C:/Users/*/AppData/Local/Microsoft/Edge/User Data/*/Session Storage + /home/*/.config/google-chrome/*/Session Storage + /home/*/.config/chrome-remote-desktop/chrome-profile/*/Session Storage + /Users/*/Library/Application Support/BraveSoftware/Brave*/*/Session Storage + /Users/*/Library/Application Support/Google/Chrome/*/Session Storage + /Users/*/Library/Application Support/Microsoft Edge/*/Session Storage + +- name: Accessor +- name: AlsoUpload + type: bool + description: If selected we also upload the Session Storage directory. + +sources: +- query: | + LET _ <= log(message="Glob %v", args= [SessionGlobs.Glob, ]) + LET _GetMapping(Data, ID) = to_dict(item={ + SELECT _key AS RawKey, + parse_string_with_regex(string=_key, + regex='map-([^-]+)-(?P.+)').Key AS _key, + utf16(string=_value) AS _value + FROM items(item=Data) + WHERE RawKey =~ format(format="map-%v", args=ID) + }) + + LET DumpSessionStorate(Data) = + SELECT parse_string_with_regex(string=_key, + regex='''namespace-(?P[^-]+)-(?P.+)''') AS Parsed, + _value, _GetMapping(Data=Data, ID=_value) AS Mapping + FROM items(item=Data) + WHERE Parsed.URL + + LET hits = SELECT OSPath, to_dict(item={ + + -- Load the whole thing into memory since we need to make + -- several passes on it. + SELECT Key AS _key, Value AS _value FROM leveldb(file=OSPath, accessor= Accessor) + }) AS Data + FROM glob(globs= SessionGlobs.Glob, accessor= Accessor) + + SELECT * FROM foreach(row={ + SELECT OSPath, Data, if(condition=AlsoUpload, then={ + SELECT upload(file=OSPath) AS Upload + FROM glob(globs="*", root=OSPath, accessor= Accessor) + }) AS Upload + FROM hits + WHERE log(message="Processing %v", args=OSPath) + + }, query={ + SELECT OSPath, + Parsed.GUID AS GUID, + Parsed.URL AS URL, + Mapping + FROM DumpSessionStorate(Data=Data) + }) diff --git a/artifacts/definitions/Generic/Applications/Office/Keywords.yaml b/artifacts/definitions/Generic/Applications/Office/Keywords.yaml index 4f41ab40d..3d61e7cdc 100644 --- a/artifacts/definitions/Generic/Applications/Office/Keywords.yaml +++ b/artifacts/definitions/Generic/Applications/Office/Keywords.yaml @@ -1,14 +1,14 @@ name: Generic.Applications.Office.Keywords description: | Microsoft Office documents among other document format (such as - LibraOffice) are actually stored in zip files. The zip file contain - the document encoded as XML in a number of zip members. + LibraOffice) are actually stored in zip files. The zip file contains + the document encoded as XML in several zip members. This makes it difficult to search for keywords within office documents because the ZIP files are typically compressed. This artifact searches for office documents by file extension and - glob then uses the zip filesystem accessor to launch a yara scan + glob then uses the zip filesystem accessor to launch a YARA scan again the uncompressed data of the document. Keywords are more likely to match when scanning the decompressed XML data. @@ -41,17 +41,18 @@ parameters: sources: - query: | - LET office_docs = SELECT FullPath AS OfficePath, + LET office_docs = SELECT OSPath AS OfficePath, Mtime as OfficeMtime, Size as OfficeSize FROM glob(globs=searchGlob + documentGlobs) // A list of zip members inside the doc that have some content. LET document_parts = SELECT OfficePath, - FullPath AS ZipMemberPath - FROM glob(globs=url( - scheme="file", path=OfficePath, fragment="/**").String, - accessor='zip') + OSPath AS ZipMemberPath + FROM glob( + globs="/**", + root=pathspec(DelegatePath=OfficePath), + accessor='zip') WHERE not IsDir and Size > 0 // For each document, scan all its parts for the keyword. @@ -60,7 +61,7 @@ sources: OfficeSize, File.ModTime as InternalMtime, String.HexData as HexContext, - File.FullPath AS FullPath + File.OSPath AS OSPath FROM foreach( row=office_docs, query={ diff --git a/artifacts/definitions/Generic/Client/CleanupTemp.yaml b/artifacts/definitions/Generic/Client/CleanupTemp.yaml new file mode 100644 index 000000000..17906e2ba --- /dev/null +++ b/artifacts/definitions/Generic/Client/CleanupTemp.yaml @@ -0,0 +1,25 @@ +name: Generic.Client.CleanupTemp +description: | + This artifact cleans up the temp folder in the Velociraptor client. + +parameters: + - name: TempGlob + default: "%TEMP%/**" + description: Glob to find all the files in the temp folder. + - name: AgeSeconds + default: 600 + type: int + description: Any files older than this many seconds will be removed. + - name: ReadllyDoIt + type: bool + +required_permissions: + - FILESYSTEM_WRITE + +sources: + - query: | + LET Threshold <= timestamp(epoch=now() - AgeSeconds ) + SELECT OSPath, Size, Mtime, + if(condition=ReadllyDoIt, then=rm(filename=OSPath)) AS Removed + FROM glob(globs=expand(path=TempGlob)) + WHERE NOT IsDir AND Mtime < Threshold diff --git a/artifacts/definitions/Generic/Client/DiskSpace.yaml b/artifacts/definitions/Generic/Client/DiskSpace.yaml index 6638b88de..69f859bd9 100644 --- a/artifacts/definitions/Generic/Client/DiskSpace.yaml +++ b/artifacts/definitions/Generic/Client/DiskSpace.yaml @@ -6,6 +6,9 @@ description: | 1. On Linux and MacOS we call `df -h`. 2. On Windows we use WMI +implied_permissions: + - EXECVE + sources: - query: | LET NonWindows = SELECT * FROM foreach(row={ diff --git a/artifacts/definitions/Generic/Client/DiskUsage.yaml b/artifacts/definitions/Generic/Client/DiskUsage.yaml new file mode 100644 index 000000000..b8a895aa6 --- /dev/null +++ b/artifacts/definitions/Generic/Client/DiskUsage.yaml @@ -0,0 +1,57 @@ +name: Generic.Client.DiskUsage +description: | + This artifact reports the amount of space used by each directory + recursively (Similar to the `du` command). + + Unlike the `du` command, this artifact can filter only certain file + name patterns. + + If you change the `TopLevelDirectory` to the drive letter + (e.g. `C:\\`) it may take a while to complete as it will need to + examine every file on the drive. + +parameters: + - name: TopLevelDirectory + default: C:/Program Files + description: The top level directory to start calculating disk usage. + + - name: FilenameGlob + default: '*' + description: A Glob expression for considering files + + - name: DirectoryGlob + default: '*' + description: A Glob expression for considering directories to recurse into. + +sources: + - query: | + LET Res <= dict() + + LET _DirInfo(DirPath) = SELECT DirPath, Size, sum(item=Size) AS TotalSize + FROM chain(a={ + SELECT Size FROM glob(globs=FilenameGlob, root=DirPath) + WHERE NOT IsDir + }, b={ + SELECT * FROM foreach(row={ + SELECT OSPath FROM glob(globs=DirectoryGlob, root=DirPath) + WHERE IsDir + }, + query={ + SELECT TotalSize AS Size FROM DirInfo(DirPath=OSPath) + }) + }) + GROUP BY 1 -- Needed for sum() + + LET DirInfo(DirPath) = SELECT * FROM _DirInfo(DirPath=DirPath) + WHERE set(item=Res, field=DirPath, + value=dict(DirPath=DirPath, TotalSize=TotalSize)) + + -- Recurse into the TopLevelDirectory and rely on the set() + -- above to store the results. + LET _ <= SELECT * FROM DirInfo(DirPath=TopLevelDirectory) + + SELECT *, humanize(bytes=TotalSize) AS TotalSizeHuman + FROM foreach(row={ + SELECT * FROM items(item=Res) + }, column="_value") + ORDER BY TotalSize DESC diff --git a/artifacts/definitions/Generic/Client/Info.yaml b/artifacts/definitions/Generic/Client/Info.yaml index 14b29fd9a..0e8f97ecd 100644 --- a/artifacts/definitions/Generic/Client/Info.yaml +++ b/artifacts/definitions/Generic/Client/Info.yaml @@ -18,22 +18,54 @@ sources: This source is used internally to populate agent info. Do not modify or remove this query. query: | + LET Interfaces = SELECT HardwareAddrString AS MAC + FROM interfaces() + WHERE HardwareAddr + SELECT config.Version.Name AS Name, config.Version.BuildTime as BuildTime, config.Version.Version as Version, config.Version.ci_build_url AS build_url, + config.Version.install_time as install_time, config.Labels AS Labels, Hostname, OS, Architecture, - Platform, PlatformVersion, KernelVersion, Fqdn + Platform, PlatformVersion, KernelVersion, Fqdn, + Interfaces.MAC AS MACAddresses FROM info() + - name: DetailedInfo + query: | + LET Info = SELECT * FROM info() + SELECT _key AS Param, _value AS Value FROM items(item=Info[0]) + + - name: LinuxInfo + description: Linux specific information about the host + precondition: SELECT OS From info() where OS = 'linux' + query: | + SELECT if(condition=version(function='sysinfo') != NULL, then=sysinfo()) AS `Computer Info`, + { SELECT Name, HardwareAddrString AS MACAddress, + Up, PointToPoint, + AddrsString AS IPAddresses + FROM interfaces() WHERE HardwareAddr} AS `Network Info` + FROM scope() + - name: WindowsInfo description: Windows specific information about the host precondition: SELECT OS From info() where OS = 'windows' query: | + LET DomainLookup <= dict( + `0`='Standalone Workstation', + `1`='Member Workstation', + `2`='Standalone Server', + `3`='Member Server', + `4`='Backup Domain Controller', + `5`='Primary Domain Controller') + SELECT { - SELECT DNSHostName, Name, Domain, TotalPhysicalMemory + SELECT DNSHostName, Name, Domain, TotalPhysicalMemory, + get(item=DomainLookup, + field=str(str=DomainRole), default="Unknown") AS DomainRole FROM wmi( query='SELECT * FROM win32_computersystem') } AS `Computer Info`, @@ -51,6 +83,30 @@ sources: } AS `Network Info` FROM scope() + notebook: + - type: vql_suggestion + name: "Enumerate Domain Roles" + template: | + /* + # Enumerate Domain Roles + + Search all clients' enrollment information for their domain roles. + */ + -- + -- Remove the below comments to label Domain Controllers + SELECT *--, label(client_id=client_id, labels="DomainController", op="set") AS Label + FROM foreach(row={ + SELECT * FROM clients() + }, query={ + SELECT + `Computer Info`.Name AS Name, client_id, + `Computer Info`.DomainRole AS DomainRole + FROM source(client_id=client_id, + flow_id=last_interrogate_flow_id, + source="WindowsInfo") + }) + -- WHERE DomainRole =~ "Controller" + - name: Users precondition: SELECT OS From info() where OS = 'windows' query: | @@ -60,9 +116,9 @@ sources: reports: - type: CLIENT template: | - {{ $client_info := Query "SELECT * FROM clients(client_id=ClientId) LIMIT 1" }} + {{ $client_info := Query "SELECT * FROM clients(client_id=ClientId) LIMIT 1" | Expand }} - {{ $flow_id := Query "SELECT timestamp(epoch=active_time / 1000000) AS Timestamp FROM flows(client_id=ClientId, flow_id=FlowId)" }} + {{ $flow_id := Query "SELECT timestamp(epoch=active_time / 1000000) AS Timestamp FROM flows(client_id=ClientId, flow_id=FlowId)" | Expand }} # {{ Get $client_info "0.os_info.fqdn" }} ( {{ Get $client_info "0.client_id" }} ) @ {{ Get $flow_id "0.Timestamp" }} @@ -74,7 +130,8 @@ reports: SELECT * FROM sample( n=4, query={ - SELECT Timestamp, rate(x=CPU, y=Timestamp) * 100 As CPUPercent, + SELECT Timestamp, + rate(x=CPU, y=Timestamp) * 100 As CPUPercent, RSS / 1000000 AS MemoryUse FROM source(artifact="Generic.Client.Stats", client_id=ClientId, @@ -83,16 +140,41 @@ reports: }) {{ end }} + {{ define "computerinfo" }} + LET X <= SELECT * + FROM source(source="LinuxInfo') + LIMIT 1 + + SELECT humanize(bytes=TotalPhysicalMemory) AS TotalPhysicalMemory, + humanize(bytes=TotalFreeMemory) AS TotalFreeMemory, + humanize(bytes=TotalSharedMemory) AS TotalSharedMemory, + humanize(bytes=TotalSwap) AS TotalSwap, + humanize(bytes=FreeSwap) AS FreeSwap + FROM foreach(row=X[0].`Computer Info`) + {{ end }} +
- {{ Query "resources" | LineChart "xaxis_mode" "time" "RSS.yaxis" 2 }} + {{ Query "resources" | TimeChart "RSS.yaxis" 2 }}
{{ $windows_info := Query "SELECT * FROM source(source='WindowsInfo')" }} - {{ if $windows_info }} + {{ if $windows_info | Expand }} # Windows agent information {{ $windows_info | Table }} {{ end }} + {{ $linux_info := Query "LET X <= SELECT * FROM source(source='LinuxInfo') LIMIT 1 SELECT * FROM X" }} + {{ if Query "SELECT * FROM source(source='LinuxInfo')" | Expand }} + # Linux agent information + + ### Network Info + {{ Query "SELECT * FROM foreach(row=X[0].`Network Info`)" | Table }} + + ### Computer Info + {{ Query "computerinfo" | Table }} + + {{ end }} + # Active Users {{ Query "SELECT * FROM source(source='Users')" | Table }} diff --git a/artifacts/definitions/Generic/Client/LocalLogs.yaml b/artifacts/definitions/Generic/Client/LocalLogs.yaml new file mode 100644 index 000000000..072c4e43f --- /dev/null +++ b/artifacts/definitions/Generic/Client/LocalLogs.yaml @@ -0,0 +1,51 @@ +name: Generic.Client.LocalLogs +description: | + Write client logs locally in an encrypted container. This helps when + we need to access what the client was doing in the past. + +type: CLIENT_EVENT + +parameters: +- name: LocalFilename + default: "%TEMP%/locallogs.log" + description: The local filename that will be written (Env variables will be expanded). +- name: MaxRows + type: int + default: "100" + description: Flush the file when we cache this many rows. +- name: MaxWait + default: "60" + type: int + description: Flush the file at least every this many seconds. +- name: MaxSize + default: "100000000" + type: int + description: Truncate the file once it reaches this length. +- name: AlsoForward + type: bool + description: | + By default we do not forward any of the logs to the server but + this allows logs to be forwarded as well as written locally. +- name: Component + default: generic + description: The log component to forward (default "generic") + type: choices + choices: + - generic + - client + - frontend + - gui + - api + +sources: +- query: | + LET _ <= log(message="Writing local log to " + expand(path=LocalFilename)) + + SELECT * FROM write_crypto_file( + max_rows=MaxRows, max_wait=MaxWait, max_size=MaxSize, + filename=expand(path=LocalFilename), + query={ + SELECT timestamp(epoch=now()) AS Timestamp, * + FROM logging(component=Component) + }) + WHERE AlsoForward diff --git a/artifacts/definitions/Generic/Client/LocalLogsRetrieve.yaml b/artifacts/definitions/Generic/Client/LocalLogsRetrieve.yaml new file mode 100644 index 000000000..f95009d63 --- /dev/null +++ b/artifacts/definitions/Generic/Client/LocalLogsRetrieve.yaml @@ -0,0 +1,28 @@ +name: Generic.Client.LocalLogsRetrieve +description: | + Retrives the locally written logs. + +type: CLIENT + +parameters: +- name: LocalFilename + default: "%TEMP%/locallogs.log" + description: The local filename that will be retrieved (Env variables will be expanded). + +sources: +- query: | + SELECT upload(file=expand(path=LocalFilename)) AS Upload + FROM scope() + notebook: + - type: vql + name: Decrypt logs + template: | + /* + # Retrieved local logs from endpoint + */ + + SELECT * FROM foreach(row={ + SELECT * FROM uploads(client_id=ClientId, flow_id=FlowId) + }, query={ + SELECT * FROM read_crypto_file(filename=vfs_path, accessor="fs") + }) diff --git a/artifacts/definitions/Generic/Client/Profile.yaml b/artifacts/definitions/Generic/Client/Profile.yaml index efc2b21ee..9831c2fce 100644 --- a/artifacts/definitions/Generic/Client/Profile.yaml +++ b/artifacts/definitions/Generic/Client/Profile.yaml @@ -16,7 +16,7 @@ description: | 3. Profile: This takes a CPU profile of the running process for the number of seconds specified in the Duration parameter. You can - read profiles using: + read profiles by using: ``` go tool pprof -callgrind -output=profile.grind profile.bin @@ -27,17 +27,22 @@ description: | at the same time since this artifacts itself will not be doing very much other than just measuring the state of the process. + NOTE: As of 0.7.0 release, this artifact will also collect + goroutines and heap profiles as distinct sources in a more readable + way. parameters: - name: Allocs description: A sampling of all past memory allocations type: bool + default: Y - name: Block description: Stack traces that led to blocking on synchronization primitives type: bool - name: Goroutine description: Stack traces of all current goroutines type: bool + default: Y - name: Heap description: A sampling of memory allocations of live objects type: bool @@ -66,14 +71,71 @@ parameters: description: Duration of sampling for Profile and Trace. default: "30" +export: | + LET CleanUp(Name) = regex_replace( + re="www.velocidex.com/golang/velociraptor/", + replace="", source=Name) + sources: - query: | - SELECT Type, - if(condition=get(field="FullPath"), - then=upload(name=Type + ".bin", file=FullPath)) AS File, - get(member="Line") AS Line + LET X = scope() + + SELECT *, X.OSPath && X.Type && upload(name=X.Type + ".bin", file=X.OSPath) AS File FROM profile(allocs=Allocs, block=Block, goroutine=Goroutine, heap=Heap, mutex=Mutex, profile=Profile, trace=Trace, logs=Logs, queries=QueryLogs, metrics=Metrics, debug=if(condition=Verbose, then=2, else=1), duration=atoi(string=Duration)) + + - name: Goroutines + query: | + -- Only show our own code. This removed unnecessary library + -- calls and cleans up the output. + SELECT *, { + SELECT format(format="%v (%v:%v)", + args=[CleanUp(Name=Name), basename(path=File), Line]) + FROM CallStack + WHERE File =~ 'velociraptor|vfilter|go-ntfs' + LIMIT 10 + } AS CallStack + FROM profile_goroutines() + WHERE CallStack + + - name: Memory + query: | + SELECT InUseBytes, InUseObjects, { + SELECT format(format="%v (%v:%v)", + args=[CleanUp(Name=Name), basename(path=File), Line]) + FROM CallStack + WHERE File =~ 'velociraptor|vfilter|go-ntfs' + LIMIT 10 + } AS CallStack + FROM profile_memory() + ORDER BY InUseBytes DESC + + - name: Logs + query: | + SELECT * FROM profile(logs=TRUE) + + - name: RunningQueries + query: | + SELECT Line.Start AS Timestamp, Line.Query AS Query + FROM profile(queries=TRUE) + WHERE NOT Line.Duration + + - name: AllQueries + query: | + SELECT Line.Start AS Timestamp, int(int = Line.Duration / 1000000) AS DurationSec, Line.Query AS Query + FROM profile(queries=TRUE) + + - name: Metrics + query: | + SELECT * + FROM profile(metrics=TRUE) + + - name: Everything + query: SELECT * FROM profile(type='.+') + +column_types: + - name: InUseBytes + type: mb diff --git a/artifacts/definitions/Generic/Client/Rekey.yaml b/artifacts/definitions/Generic/Client/Rekey.yaml new file mode 100644 index 000000000..c3ad62272 --- /dev/null +++ b/artifacts/definitions/Generic/Client/Rekey.yaml @@ -0,0 +1,28 @@ +name: Generic.Client.Rekey +description: | + This artifact forces the client to regenerate its client id. + + This is normally not needed! You will only need to use this artifact in very + specific situations, such as when the Velociraptor client was accidentally + incorporated into a VM image with an existing writeback file. This will cause + multiple cloned systems to connect with the same client id, and the server + will then reject those clients with a HTTP "409 Rejected" message. + + If this happens, you can use the `Server.Monitor.ClientConflict` artifact to + schedule collection of this artifact against rejected clients automatically. + + The `Wait` parameter controls how long we wait before restarting the client. + Reduce this number if you need to rekey a lot of clients quickly. + +required_permissions: + - EXECVE + +parameters: + - name: Wait + description: Wait this long before restarting the client. + type: int + default: '10' + +sources: + - query: + SELECT rekey(wait=Wait) FROM scope() diff --git a/artifacts/definitions/Generic/Client/Stats.yaml b/artifacts/definitions/Generic/Client/Stats.yaml index d39c439f5..54dcbf2c6 100644 --- a/artifacts/definitions/Generic/Client/Stats.yaml +++ b/artifacts/definitions/Generic/Client/Stats.yaml @@ -1,17 +1,27 @@ name: Generic.Client.Stats -description: An Event artifact which generates client's CPU and memory statistics. +description: | + An Event artifact which records client's CPU and memory + statistics. + + To learn about managing end point performance with Velociraptor + see this [blog + post](https://docs.velociraptor.app/blog/html/2019/02/10/velociraptor_performance/). + parameters: - name: Frequency description: Return stats every this many seconds. + type: int default: "10" type: CLIENT_EVENT sources: - precondition: SELECT OS From info() where OS = 'windows' query: | - SELECT * from foreach( + SELECT *, rate(x=CPU, y=Timestamp) AS CPUPercent + FROM foreach( row={ - SELECT UnixNano FROM clock(period=atoi(string=Frequency)) + SELECT UnixNano + FROM clock(period=Frequency) }, query={ SELECT UnixNano / 1000000000 as Timestamp, @@ -20,11 +30,30 @@ sources: FROM pslist(pid=getpid()) }) + notebook: + - type: vql_suggestion + name: Graph CPU usage + template: | + /* + # Events from Generic.Client.Stats + */ + LET resources = SELECT Timestamp, rate(x=CPU, y=Timestamp) * 100 As CPUPercent, + RSS / 1000000 AS MemoryUse + FROM source(start_time=StartTime, end_time=EndTime) + WHERE CPUPercent >= 0 + /* + {{ Query "SELECT * FROM resources" | LineChart "xaxis_mode" "time" "RSS.yaxis" 2 }} + */ + SELECT * FROM resources + LIMIT 50 + - precondition: SELECT OS From info() where OS != 'windows' query: | - SELECT * from foreach( + SELECT *, rate(x=CPU, y=Timestamp) AS CPUPercent + FROM foreach( row={ - SELECT UnixNano FROM clock(period=atoi(string=Frequency)) + SELECT UnixNano + FROM clock(period=Frequency) }, query={ SELECT UnixNano / 1000000000 as Timestamp, @@ -84,9 +113,6 @@ reports: {{ template "resources" }} ``` - > To learn about managing end point performance with Velociraptor see - the [blog post](https://docs.velociraptor.velocidex.com/blog/html/2019/02/10/velociraptor_performance.html). - column_types: - name: Timestamp type: timestamp diff --git a/artifacts/definitions/Generic/Client/Trace.yaml b/artifacts/definitions/Generic/Client/Trace.yaml new file mode 100644 index 000000000..0a11b0138 --- /dev/null +++ b/artifacts/definitions/Generic/Client/Trace.yaml @@ -0,0 +1,24 @@ +name: Generic.Client.Trace +description: | + This artifact collects profiling information about the running + client. The artifact is automatically added when the GUI selects a + non zero Trace frequency. + + NOTE: You can also add the artifact directly, but then you will need + to cancel the collection manually since it will continue to run + until the timeout is reached. + + Minimum Version: 0.6.8 + +parameters: +- name: FrequencySec + type: int + default: 10 + +sources: +- query: | + SELECT * FROM if(condition=version(function="trace"), + then={ + SELECT trace() AS TraceFile + FROM clock(start=0, period=FrequencySec) + }) diff --git a/artifacts/definitions/Generic/Client/VQL.yaml b/artifacts/definitions/Generic/Client/VQL.yaml index 8c6e3302b..84e1dd4c7 100644 --- a/artifacts/definitions/Generic/Client/VQL.yaml +++ b/artifacts/definitions/Generic/Client/VQL.yaml @@ -3,7 +3,7 @@ description: | Run arbitrary VQL on the endpoint. required_permissions: - - EXECVE + - IMPERSONATION parameters: - name: Command @@ -11,4 +11,4 @@ parameters: sources: - query: | - SELECT * FROM query(query=Command) + SELECT * FROM query(query=Command, env=dict(config=config)) diff --git a/artifacts/definitions/Generic/Collectors/File.yaml b/artifacts/definitions/Generic/Collectors/File.yaml index 42f224edb..f2f89b763 100755 --- a/artifacts/definitions/Generic/Collectors/File.yaml +++ b/artifacts/definitions/Generic/Collectors/File.yaml @@ -4,6 +4,9 @@ description: | device. The globs will be searched in one pass - so you can provide many globs at the same time. +aliases: + - Windows.Collectors.File + parameters: - name: collectionSpec description: | @@ -13,62 +16,106 @@ parameters: default: | Glob Users\*\NTUser.dat + - name: Root description: | - On Windows, this is the device to apply all the glob on. On *NIX, - this should be a path to a subdirectory but must not be a real - device from /dev. + On Windows, this is the device to apply all the glob on + (e.g. `C:`). On *NIX, this should be a path to a subdirectory or + /. default: "C:" + - name: Accessor - default: lazy_ntfs + default: auto description: | - On Windows, this can be left on `lazy_ntfs'. For *NIX, this value - must be set to `file' since the ntfs accessors are not available. - - name: Separator + On Windows, this can be changed to `ntfs`. + + - name: NTFS_CACHE_TIME + type: int + description: How often to flush the NTFS cache. (Default is never). + default: "1000000" + + - name: UPLOAD_IS_RESUMABLE + type: bool + default: N + description: | + If set the uploads can be resumed if the flow times out or errors. + + - name: MaxFileSize + type: int + default: 18446744073709551615 description: | - The path separator used to construct the final globs from the root - and the partial globs in `collectionSpec'. - default: "\\" + The max size in bytes of the individual files to collect. + Set to 0 to disable it. + sources: - name: All Matches Metadata query: | - -- Generate the collection globs for each device - LET specs = SELECT Root + Separator + Glob AS Glob - FROM collectionSpec - WHERE log(message=format(format="Processing Device %v with %v: %v", - args=[Root, Accessor, Glob])) - - -- Join all the collection rules into a single Glob plugin. This ensure we - -- only make one pass over the filesystem. We only want LFNs. - LET hits = SELECT FullPath AS SourceFile, Size, - Ctime AS Created, - Mtime AS Modified, - Atime AS LastAccessed - FROM glob(globs=specs.Glob, accessor=Accessor) - WHERE NOT IsDir AND log(message="Found " + SourceFile) - - -- Create a unique key to group by - modification time and path name. - LET all_results <= SELECT Created, LastAccessed, - Modified, Size, SourceFile - FROM hits - - SELECT * FROM all_results + LET RootPath <= pathspec(Path=Root, accessor=Accessor) + + -- Generate the collection globs for each device + LET specs = SELECT RootPath + Glob AS Glob + FROM collectionSpec + WHERE log(message=format(format="Processing Device %v with %v: glob is %v", + args=[Root, Accessor, Glob])) + + -- Join all the collection rules into a single Glob plugin. This ensure we + -- only make one pass over the filesystem. We only want LFNs. + LET hits = SELECT OSPath AS SourceFile, + Size, + Btime AS Created, + Ctime AS Changed, + Mtime AS Modified, + Atime AS LastAccessed + FROM glob(globs=specs.Glob, accessor=Accessor) + WHERE NOT IsDir + AND log(message="Found " + SourceFile) + AND ( Size <= MaxFileSize OR + ( log(message="Skipping file " + SourceFile + " Due to MaxFileSize") + AND FALSE )) + + -- Pass all the results to the next query. This will serialize + -- to disk if there are too many results. + LET all_results <= SELECT Created, + Changed, + LastAccessed, + Modified, + Size, + SourceFile + FROM hits + + SELECT * + FROM all_results + - name: Uploads query: | - -- Upload the files - LET uploaded_files = SELECT * FROM foreach(row=all_results, - workers=30, - query={ - SELECT Created, LastAccessed, Modified, SourceFile, Size, - upload(file=SourceFile, accessor=Accessor, name=SourceFile, - mtime=Modified) AS Upload + -- Upload the files. Split into workers so the files are uploaded in parallel. + LET uploaded_files = SELECT * + FROM foreach(row={ + SELECT * + FROM all_results + }, + workers=30, + query={ + SELECT Created, + Changed, + LastAccessed, + Modified, + SourceFile, + Size, + upload(file=SourceFile, accessor=Accessor, mtime=Modified) AS Upload FROM scope() - }) + }) - -- Separate the hashes into their own column. - SELECT now() AS CopiedOnTimestamp, SourceFile, Upload.Path AS DestinationFile, - Size AS FileSize, Upload.sha256 AS SourceFileSha256, - Created, Modified, LastAccessed + -- Separate the hashes into their own column. + SELECT now() AS CopiedOnTimestamp, + SourceFile, + Upload.Path AS DestinationFile, + Size AS FileSize, + Upload.sha256 AS SourceFileSha256, + Created, + Changed, + Modified, + LastAccessed FROM uploaded_files diff --git a/artifacts/definitions/Generic/Detection/HashHunter.yaml b/artifacts/definitions/Generic/Detection/HashHunter.yaml new file mode 100644 index 000000000..cf7481b33 --- /dev/null +++ b/artifacts/definitions/Generic/Detection/HashHunter.yaml @@ -0,0 +1,101 @@ +name: Generic.Detection.HashHunter +author: "Matt Green - @mgreen27" +description: | + This artifact enables searching for hashes. + + The artifact takes a glob targeting input, then generates a hash for each + file in scope to compare to several types of hash lists provided by the user. + + Note: this artifacts filters are cumulative so a hash based hit will return + no results if the file is filtered out by other filters. + For most performant searches use path, size and and date filters. By default + the artifact uses the 'auto' data accessor but can also be changed as desired. + +parameters: + - name: TargetGlob + description: Glob to target. + default: "C:/Users/**/*" + - name: Accessor + description: Velociraptor accessor to use. Changing to ntfs will increase scan time. + default: auto + - name: DateAfter + description: Search for binaries with timestamps after this date. YYYY-MM-DDTmm:hh:ssZ + type: timestamp + - name: DateBefore + description: Search for binaries with timestamps before this date. YYYY-MM-DDTmm:hh:ssZ + type: timestamp + - name: SizeMax + description: Return binaries only under this size in bytes. + type: int64 + default: 4294967296 + - name: SizeMin + description: Return binaries only over this size in bytes. + type: int64 + default: 0 + - name: MD5List + description: MD5 hash list to hunt for. New MD5 hash on each line + default: + - name: SHA1List + description: SHA1 hash list to hunt for. New SHA1 hash on each line + default: + - name: SHA256List + description: SHA256 hash list to hunt for. New SHA256 hash on each line + default: + +sources: + - query: | + -- setup hash lists + LET MD5List <= if(condition= MD5List, + then= split(sep='\\s+',string=MD5List), else=Null) + LET SHA1List <= if(condition= SHA1List, + then= split(sep='\\s+',string=SHA1List), else=Null) + LET SHA256List <= if(condition= SHA256List, + then= split(sep='\\s+',string=SHA256List), else=Null) + + -- set hash selector for optimized hash calculation + LET HashSelector <= SELECT * FROM chain( + a={ SELECT "MD5" AS Hash FROM scope() WHERE MD5List }, + b={ SELECT "SHA1" AS Hash FROM scope() WHERE SHA1List }, + c={ SELECT "SHA256" AS Hash FROM scope() WHERE SHA256List }) + + -- firstly find files in scope with performance + LET find_files = SELECT * FROM if(condition=DateBefore AND DateAfter, + then={ + SELECT OSPath, Name, Size,Mtime,Atime,Ctime,Btime + FROM glob(globs=TargetGlob,accessor=Accessor,nosymlink='True') + WHERE NOT IsDir AND NOT IsLink + AND Size > SizeMin AND Size < SizeMax + AND ( Mtime < DateBefore OR Ctime < DateBefore OR Btime < DateBefore ) + AND ( Mtime > DateAfter OR Ctime > DateAfter OR Btime > DateAfter ) + }, + else={ SELECT * FROM if(condition=DateBefore, + then={ + SELECT OSPath, Name, Size,Mtime,Atime,Ctime,Btime + FROM glob(globs=OSPath,accessor=Accessor) + WHERE NOT IsDir AND NOT IsLink + AND Size > SizeMin AND Size < SizeMax + AND ( Mtime < DateBefore OR Ctime < DateBefore OR Btime < DateBefore ) + }, + else={ SELECT * FROM if(condition=DateAfter, + then={ + SELECT OSPath, Name, Size,Mtime,Atime,Ctime,Btime + FROM glob(globs=TargetGlob,accessor=Accessor) + WHERE NOT IsDir AND NOT IsLink + AND Size > SizeMin AND Size < SizeMax + AND ( Mtime > DateAfter OR Ctime > DateAfter OR Btime > DateAfter ) + }, + else={ + SELECT OSPath, Name, Size,Mtime,Atime,Ctime,Btime + FROM glob(globs=TargetGlob,accessor=Accessor) + WHERE NOT IsDir AND NOT IsLink + AND Size > SizeMin AND Size < SizeMax + })})}) + + + -- lookup hash and run finl filters + SELECT OSPath,Name,Size, + dict(Mtime=Mtime,Atime=Atime,Ctime=Ctime,Btime=Btime) as Timestamps, + hash(path=OSPath,hashselect=HashSelector.Hash) as Hash + FROM if(condition= HashSelector.Hash, then= find_files) + WHERE + ( Hash.MD5 in MD5List OR Hash.SHA1 in SHA1List OR Hash.SHA256 in SHA256List ) \ No newline at end of file diff --git a/artifacts/definitions/Generic/Detection/Logs.yaml b/artifacts/definitions/Generic/Detection/Logs.yaml new file mode 100644 index 000000000..228a03d85 --- /dev/null +++ b/artifacts/definitions/Generic/Detection/Logs.yaml @@ -0,0 +1,80 @@ +name: Generic.Detection.Logs +author: "Matt Green - @mgreen27, Apache groks thanks to Harsh Jaroli and Krishna Patel" +description: | + This artifact enables grep of Logs to hunt for strings of interest. Default + target glob includes /var/log/, Apache and Windows IIS paths. + + Parameters include SearchRegex and WhitelistRegex as regex terms and will + return the whole line to assist with scoping. + + IIS and Apache Groks are available as notebook suggestions - please feel free to PR + additions! + + +parameters: + - name: TargetGlob + default: '/{/var/log/**,*:/inetpub/logs/**/,{/var/log/httpd,/var/log/apache2,/var/log/nginx,C:/Apache/logs}/{access.log,access_log}*}' + - name: SearchRegex + description: "Regex of strings to search in line." + default: 'PUT ' + type: regex + - name: WhitelistRegex + description: "Regex of strings to leave out of output." + default: + type: regex + +sources: + - query: | + LET files = SELECT OSPath FROM glob(globs=TargetGlob) + + SELECT * FROM foreach(row=files, + query={ + SELECT Line, OSPath + FROM parse_lines(filename=OSPath) + WHERE + Line =~ SearchRegex + AND NOT if(condition= WhitelistRegex, + then= Line =~ WhitelistRegex, + else= FALSE) + }) + + notebook: + - type: vql_suggestion + name: IIS Groks + template: | + /* + ### IIS grok + + Note: IIS doesn't have a standard logging format so we have added some + suggestions. Comment in preferred or add / modify your own. + */ + + LET target_grok = "%{TIMESTAMP_ISO8601:LogTimeStamp} %{IPORHOST:Site} %{WORD:Method} %{URIPATH:UriPath} %{NOTSPACE:QueryString} %{NUMBER:Port} %{NOTSPACE:Username} %{IPORHOST:Clienthost} %{NOTSPACE:Useragent} %{NOTSPACE:Referrer} %{NUMBER:Response} %{NUMBER:Subresponse} %{NUMBER:Win32status} %{NUMBER:Timetaken:int}" + --LET target_grok = "%{TIMESTAMP_ISO8601:log_timestamp} %{IPORHOST:site} %{WORD:method} %{URIPATH:page} %{NOTSPACE:querystring} %{NUMBER:port} %{NOTSPACE:username} %{IPORHOST:clienthost} %{NOTSPACE:useragent} %{NOTSPACE:referer} %{NUMBER:response} %{NUMBER:subresponse} %{NUMBER:scstatus} %{NUMBER:timetaken:int}" + --LET target_grok = "%{TIMESTAMP_ISO8601:log_timestamp} %{WORD:iisSite} %{NOTSPACE:computername} %{IPORHOST:site} %{WORD:method} %{URIPATH:page} %{NOTSPACE:querystring} %{NUMBER:port} %{NOTSPACE:username} %{IPORHOST:clienthost} %{NOTSPACE:protocol} %{NOTSPACE:useragent} %{NOTSPACE:referer} %{IPORHOST:cshost} %{NUMBER:response} %{NUMBER:subresponse} %{NUMBER:scstatus} %{NUMBER:bytessent:int} %{NUMBER:bytesrecvd:int} %{NUMBER:timetaken:int}" + + + LET parsed = SELECT ClientId as _ClientId, Line as _Raw, + grok(data=Line,grok=target_grok) as GrokParsed + FROM source() + WHERE GrokParsed + + SELECT * FROM foreach(row=parsed, + query={ SELECT *, _Raw FROM GrokParsed }) + + - type: vql_suggestion + name: Apache Groks + template: | + /* + ### Apache Grok + */ + + LET target_grok = '''%{IPORHOST:client} - - \[%{HTTPDATE:timestamp}\] "%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:httpversion}" %{NUMBER:status} %{NUMBER:response_size}''' + + LET parsed = SELECT ClientId as _ClientId, Line as _Raw, + grok(data=Line,grok=target_grok) as GrokParsed + FROM source() + WHERE GrokParsed + + SELECT * FROM foreach(row=parsed, + query={ SELECT *, _Raw FROM GrokParsed }) diff --git a/artifacts/definitions/Generic/Detection/Yara/Glob.yaml b/artifacts/definitions/Generic/Detection/Yara/Glob.yaml index 42caa2aec..649d21328 100644 --- a/artifacts/definitions/Generic/Detection/Yara/Glob.yaml +++ b/artifacts/definitions/Generic/Detection/Yara/Glob.yaml @@ -1,33 +1,40 @@ name: Generic.Detection.Yara.Glob author: Matt Green - @mgreen27 description: | - This artifact returns a list of target files then runs Yara over the target + This artifact returns a list of target files then runs YARA over the target list. - There are 2 kinds of Yara rules that can be deployed: + There are 2 kinds of YARA rules that can be deployed: - 1. Url link to a yara rule. - 2. or a Standard Yara rule attached as a parameter. + 1. Url link to a YARA rule. + 2. or a Standard YARA rule attached as a parameter. - Only one method of Yara will be applied and search order is as above. + Only one method of YARA will be applied and search order is as above. - The artifact leverages Glob for search so relevant filters can be applied + The artifact uses Glob for search so relevant filters can be applied including Glob, Size and date. Date filters will target files with a timestamp before LatestTime and after EarliestTime. The artifact also has an option to - upload any files with Yara hits. + upload any files with YARA hits. Some examples of path glob may include: * Specific binary: `/usr/bin/ls` * Wildcards: `/var/www/*.js` * More wildcards: `/var/www/**/*.js` - * Multiple extentions: `/var/www/*\.{php,aspx,js,html}` + * Multiple extensions: `/var/www/*\.{php,aspx,js,html}` * Windows: `C:/Users/**/*.{exe,dll,ps1,bat}` + * Windows: `C:\Users\**\*.{exe,dll,ps1,bat}` NOTE: this artifact runs the glob plugin with the nosymlink switch turned on. This will NOT follow any symlinks and may cause unexpected results if unknowingly targeting a folder with symlinks. + If upload is selected NumberOfHits is redundant and not advised as hits are + grouped by path to ensure files only downloaded once. +aliases: + - Windows.Detection.Yara.Glob + - Linux.Detection.Yara.Glob + - MacOS.Detection.Yara.Glob type: CLIENT parameters: @@ -36,8 +43,10 @@ parameters: default: /usr/bin/ls - name: SizeMax description: maximum size of target file. + type: int64 - name: SizeMin description: minimum size of target file. + type: int64 - name: UploadHits type: bool - name: DateAfter @@ -48,7 +57,7 @@ parameters: description: "search for events before this date. YYYY-MM-DDTmm:hh:ssZ" - name: YaraUrl description: If configured will attempt to download Yara rules form Url - default: + type: upload - name: YaraRule type: yara description: Final Yara option and the default if no other options provided. @@ -61,13 +70,19 @@ parameters: condition: uint32(0) == 0x464c457f } + - name: NumberOfHits + description: This artifact will stop by default at one hit. This setting allows additional hits + default: 1 + type: int + - name: ContextBytes + description: Include this amount of bytes around hit as context. + default: 0 + type: int sources: - query: | -- check which Yara to use - LET yara = SELECT * FROM if(condition=YaraUrl, - then= { SELECT Content FROM http_client( url=YaraUrl, method='GET') }, - else= { SELECT YaraRule as Content FROM scope() }) + LET yara_rules <= YaraUrl || YaraRule -- time testing LET time_test(stamp) = @@ -83,7 +98,7 @@ sources: ))) -- first find all matching glob - LET files = SELECT FullPath, Name, Size, Mtime, Atime, Ctime, Btime + LET files = SELECT OSPath, Name, Size, Mtime, Atime, Ctime, Btime FROM glob(globs=PathGlob,nosymlink='True') WHERE NOT IsDir AND NOT IsLink @@ -99,26 +114,40 @@ sources: OR time_test(stamp=Ctime) OR time_test(stamp=Btime)) - -- scan files and only report a single hit. + -- scan files and prepare hit metadata LET hits = SELECT * FROM foreach(row=files, query={ SELECT - FileName as FullPath, - File.Size AS Size, + OSPath, + File.Size as Size, Mtime, Atime, Ctime, Btime, Rule, Tags, Meta, - str(str=String.Data) AS HitContext, - String.Offset AS HitOffset - FROM yara(rules=yara.Content[0],files=FullPath) - LIMIT 1 + String.Name as YaraString, + String.Offset as HitOffset, + upload( accessor='scope', + file='String.Data', + name=format(format="%v-%v-%v", + args=[ + OSPath, + if(condition= String.Offset - ContextBytes < 0, + then= 0, + else= String.Offset - ContextBytes), + if(condition= String.Offset + ContextBytes > Size, + then= Size, + else= String.Offset + ContextBytes) ] + )) as HitContext + FROM yara(rules=yara_rules,files=OSPath, + context=ContextBytes,number=NumberOfHits) }) - -- upload files that have hit - LET upload_hits=SELECT *, - upload(file=FullPath) AS Upload - FROM hits + -- upload files if selected + LET upload_hits = SELECT *, upload(file=OSPath,name=OSPath) as Upload FROM hits -- return rows - SELECT * FROM if(condition=UploadHits, - then=upload_hits, - else=hits) + SELECT * FROM if(condition= UploadHits, + then= upload_hits, + else= hits ) + +column_types: + - name: HitContext + type: preview_upload \ No newline at end of file diff --git a/artifacts/definitions/Generic/Detection/Yara/Zip.yaml b/artifacts/definitions/Generic/Detection/Yara/Zip.yaml new file mode 100644 index 000000000..a45f91e72 --- /dev/null +++ b/artifacts/definitions/Generic/Detection/Yara/Zip.yaml @@ -0,0 +1,137 @@ +name: Generic.Detection.Yara.Zip +author: "Matt Green - @mgreen27" +description: | + This artifact enables running YARA on embedded compressed files. + + The artifact: + + * firstly searches for compressed zip files (PK header) + * then applies YARA on files inside. + * files matching ZipFilenameRegex are recursively searched as above. + + The artifact is optimized to recursively search through embedded zip, + jar,war and ear files by extracting any discovered containers. + Select UploadHits to upload Discovered file for further analysis. It is + recommended to increase default artifact timeout for large servers or target + glob. + + Some examples of path glob may include: + + * Specific container: `/path/here/file.zip` + * Wildcards: `/var/www/*.{jar,war,ear}` + * More wildcards: `/var/www/**/*.jar` + * Windows: `C:/**/*.zip` + + NOTE: this artifact runs the glob plugin with the nosymlink switch + turned on. This will NOT follow any symlinks and may cause + unexpected results if unknowingly targeting a folder with + symlinks. YARA is not applied to the containers, only contained contents + that are not containers. + +parameters: + - name: TargetGlob + default: "**/*.{zip,jar,war,ear}" + - name: ZipFilenameRegex + default: ".(zip|jar|war|ear)$" + description: Regex of FileName inside container files we would like to recursively scan. + - name: MaxRecursions + description: Number of recursions to allow checking inside archives. Default is 10 layers. + default: 10 + type: int + - name: UploadHits + description: Select to upload hits to server. + type: bool + - name: YaraRule + type: yara + description: Final Yara option and the default if no other options provided. + default: | + rule IsPE:TestRule { + meta: + author = "the internet" + date = "2021-03-04" + description = "A simple PE rule to test yara features" + condition: + uint16(0) == 0x5A4D and + uint32(uint32(0x3C)) == 0x00004550 + } + - name: NumberOfHits + description: THis artifact will stop by default at one hit. This setting allows additional hits + default: 1 + type: int + - name: ContextBytes + description: Include this amount of bytes around hit as context. + default: 0 + type: int + +sources: + - query: | + -- this section glob searches and confirms we are looking at zip container + LET target_files = SELECT *, + read_file(filename=OSPath,offset=0,length=2) as _Header + FROM glob(globs=TargetGlob,nosymlink=True) + WHERE _Header = 'PK' + + -- recursive search function + LET Recurse(Container, File, Accessor, RecursionRounds) = SELECT * FROM if( + condition=RecursionRounds < MaxRecursions, + then={ + SELECT * FROM foreach( + row={ + SELECT * + FROM glob(accessor='zip', + root=pathspec(DelegatePath=File, DelegateAccessor=Accessor), + globs='**') + WHERE NOT IsDir AND Size > 0 + }, + query={ + SELECT * + FROM if(condition=Name =~ ZipFilenameRegex, + then={ + SELECT * + FROM Recurse( + Container = Container, + File=OSPath, + Accessor="zip", + RecursionRounds = RecursionRounds + 1) + }, + else={ + SELECT + Container, + OSPath.HumanString as ExtractedPath, + OSPath.Path as FilePath, + hash(accessor='zip',path=OSPath) as Hash, + File.Size AS Size, + Mtime, Atime, Ctime, Btime, + Rule, Tags, Meta, + String.Name as YaraString, + String.Offset as HitOffset, + if(condition=String.Data, + then=upload( + accessor='scope', + file='String.Data', + name=format(format="%v_%v", + args=[ OSPath.HumanString, String.Offset ] + ))) as HitContext + FROM yara(accessor='zip',files=OSPath,rules=YaraRule, + context=ContextBytes, number=NumberOfHits) + }) + }) + }) + + LET hits = SELECT * FROM foreach(row=target_files, + query={ + SELECT * + FROM Recurse(Container=OSPath,File=OSPath, Accessor="auto", RecursionRounds=0) + }) + + -- upload files that have hit + LET upload_hits = SELECT *, upload(file=Container) as ContainerUpload FROM hits + + -- display rows + SELECT * FROM if(condition=UploadHits, + then= upload_hits, + else= hits) + +column_types: + - name: HitContext + type: preview_upload diff --git a/artifacts/definitions/Generic/Forensic/Carving/URLs.yaml b/artifacts/definitions/Generic/Forensic/Carving/URLs.yaml index 13f0a372a..8ba264021 100644 --- a/artifacts/definitions/Generic/Forensic/Carving/URLs.yaml +++ b/artifacts/definitions/Generic/Forensic/Carving/URLs.yaml @@ -20,13 +20,13 @@ parameters: sources: - query: | - LET matching = SELECT FullPath FROM glob( + LET matching = SELECT OSPath FROM glob( globs=parse_json_array(data=UrlGlob)) - SELECT FullPath, URL FROM foreach( + SELECT OSPath, URL FROM foreach( row=matching, query={ - SELECT FullPath, - URL FROM parse_records_with_regex(file=FullPath, + SELECT OSPath, + URL FROM parse_records_with_regex(file=OSPath, regex="(?Phttps?:\\/\\/[\\w\\.-]+[\\/\\w \\.-]*)") }) diff --git a/artifacts/definitions/Generic/Forensic/HashLookup.yaml b/artifacts/definitions/Generic/Forensic/HashLookup.yaml index 2937cf327..ea64954df 100644 --- a/artifacts/definitions/Generic/Forensic/HashLookup.yaml +++ b/artifacts/definitions/Generic/Forensic/HashLookup.yaml @@ -5,8 +5,8 @@ description: | this artifact (e.g. with an external program using the API) to lookup the hashes with an external service. - You can also send hashes to this artifact yourself using the - `send_event()` vql Function. For example, the following will add + You can also send hashes to this artifact yourself by using the + `send_event()` VQL function. For example, the following will add hashes from the results of another artifact. ```vql diff --git a/artifacts/definitions/Generic/Forensic/LocalHashes/Glob.yaml b/artifacts/definitions/Generic/Forensic/LocalHashes/Glob.yaml index ab705e818..75b4e167f 100644 --- a/artifacts/definitions/Generic/Forensic/LocalHashes/Glob.yaml +++ b/artifacts/definitions/Generic/Forensic/LocalHashes/Glob.yaml @@ -1,20 +1,20 @@ name: Generic.Forensic.LocalHashes.Glob description: | This artifact maintains a local (client side) database of file - hashes. It is then possible to query this database using the - Generic.Forensic.LocalHashes.Query artifact + hashes. It is then possible to query this database by using the + `Generic.Forensic.LocalHashes.Query` artifact Maintaining hashes client side allows Velociraptor to answer the query - which machine has this hash on our network extremely quickly. Velociraptor only needs to lookup the each client's local database of file hashes. - Maintaining this database case be done using this artifact or using - the Windows.Forensics.LocalHashes.Usn artifact. + Maintaining this database case be done by using this artifact or by using + the `Windows.Forensics.LocalHashes.Usn` artifact. This artifact simply crawls the filesystem hashing files as specified by the glob expression, and adds them to the local hash - database. You can rate limit this artifact using the ops/sec setting + database. You can rate limit this artifact by using the ops/sec setting to perform a slow update of the local file hash database. parameters: @@ -32,26 +32,26 @@ parameters: sources: - query: | - LET hash_db <= SELECT FullPath + LET hash_db <= SELECT OSPath FROM Artifact.Generic.Forensic.LocalHashes.Init(HashDb=HashDb) - LET path <= hash_db[0].FullPath + LET path <= hash_db[0].OSPath LET _ <= log(message="Will use local hash database " + path) // Crawl the files and calculate their hashes - LET files = SELECT FullPath, Size, hash(path=FullPath).MD5 AS Hash + LET files = SELECT OSPath, Size, hash(path=OSPath).MD5 AS Hash FROM glob(globs=HashGlob) WHERE Mode.IsRegular - LET insertion = SELECT FullPath, Hash, Size, { + LET insertion = SELECT OSPath, Hash, Size, { SELECT * FROM sqlite(file=path, query="INSERT into hashes (path, md5, timestamp, size) values (?,?,?,?)", - args=[FullPath, Hash, now(), Size]) + args=[OSPath.String, Hash, now(), Size]) } AS Insert FROM files WHERE Insert OR TRUE - SELECT FullPath, Hash, Size + SELECT OSPath, Hash, Size FROM insertion WHERE NOT SuppressOutput diff --git a/artifacts/definitions/Generic/Forensic/LocalHashes/Init.yaml b/artifacts/definitions/Generic/Forensic/LocalHashes/Init.yaml index 8e5bd0886..acb83295c 100644 --- a/artifacts/definitions/Generic/Forensic/LocalHashes/Init.yaml +++ b/artifacts/definitions/Generic/Forensic/LocalHashes/Init.yaml @@ -8,6 +8,9 @@ parameters: description: Name of the local hash database default: hashdb.sqlite +implied_permissions: + - FILESYSTEM_WRITE + sources: - query: | LET SQL = " @@ -30,4 +33,4 @@ sources: SELECT * FROM sqlite(file=hash_db, query=Line) }) - SELECT hash_db AS FullPath FROM scope() + SELECT hash_db AS OSPath FROM scope() diff --git a/artifacts/definitions/Generic/Forensic/LocalHashes/Query.yaml b/artifacts/definitions/Generic/Forensic/LocalHashes/Query.yaml index 6c9d3dd1a..5d2b3f8f1 100644 --- a/artifacts/definitions/Generic/Forensic/LocalHashes/Query.yaml +++ b/artifacts/definitions/Generic/Forensic/LocalHashes/Query.yaml @@ -1,11 +1,11 @@ name: Generic.Forensic.LocalHashes.Query description: | This artifact maintains a local (client side) database of file - hashes. It is then possible to query this database using the + hashes. It is then possible to query this database by using the Generic.Forensic.LocalHashes.Query artifact. NOTE: This artifact expects a CSV file with one hash per line. On - the command line you can encode carriage return using powershell + the command line you can encode carriage return by using PowerShell like this: ``` @@ -30,7 +30,7 @@ parameters: sources: - query: | - LET hash_db <= SELECT FullPath + LET hash_db <= SELECT OSPath FROM Artifact.Generic.Forensic.LocalHashes.Init(HashDb=HashDb) -- Check hashes from the CSV or comma delimited input @@ -49,7 +49,7 @@ sources: query={ SELECT path AS Path, md5 AS MD5, size AS Size, timestamp(epoch=time) AS Timestamp - FROM sqlite(file=hash_db[0].FullPath, + FROM sqlite(file=hash_db[0].OSPath, query="SELECT path, md5, size, timestamp AS time FROM hashes WHERE md5 = ?", args=Hash) }) diff --git a/artifacts/definitions/Generic/Forensic/Timeline.yaml b/artifacts/definitions/Generic/Forensic/Timeline.yaml index 2d541054a..684b861ac 100644 --- a/artifacts/definitions/Generic/Forensic/Timeline.yaml +++ b/artifacts/definitions/Generic/Forensic/Timeline.yaml @@ -16,7 +16,7 @@ sources: - precondition: SELECT OS From info() where OS = 'windows' AND timelineAccessor = 'ntfs' query: | - SELECT 0 AS Md5, FullPath, + SELECT 0 AS Md5, OSPath, Sys.mft as Inode, Mode.String AS Mode, 0 as Uid, 0 as Gid, Size, Atime, Mtime, Ctime @@ -26,7 +26,7 @@ sources: - precondition: SELECT * From scope() where timelineAccessor = 'file' query: | - SELECT 0 AS Md5, FullPath, + SELECT 0 AS Md5, OSPath, Sys.Ino as Inode, Mode.String AS Mode, Sys.Uid AS Uid, Sys.Gid AS Gid, Size, Atime, Mtime, Ctime diff --git a/artifacts/definitions/Generic/Network/InterfaceAddresses.yaml b/artifacts/definitions/Generic/Network/InterfaceAddresses.yaml new file mode 100644 index 000000000..7d48dda10 --- /dev/null +++ b/artifacts/definitions/Generic/Network/InterfaceAddresses.yaml @@ -0,0 +1,19 @@ +name: Generic.Network.InterfaceAddresses +description: | + Network interfaces and relevant metadata. This artifact works on all + supported OSs. + +aliases: + - Windows.Network.InterfaceAddresses + +sources: + - query: | + LET interface_address = + SELECT Index, MTU, Name, + HardwareAddr.String AS HardwareAddr, + Flags, Addrs + from interfaces() + + SELECT Index, MTU, Name, HardwareAddr, + Flags, Addrs.IP as IP, Addrs.Mask.String as Mask + FROM flatten(query=interface_address) diff --git a/artifacts/definitions/Generic/System/EfiSignatures.yaml b/artifacts/definitions/Generic/System/EfiSignatures.yaml new file mode 100644 index 000000000..14f12e608 --- /dev/null +++ b/artifacts/definitions/Generic/System/EfiSignatures.yaml @@ -0,0 +1,115 @@ +name: Generic.System.EfiSignatures +description: | + Collect Efi Signature information from the client. + +type: CLIENT +export: | + -- GUIDs are taken from + -- https://github.com/chipsec/chipsec/blob/main/chipsec/hal/uefi_common.py + LET PROFILE = '''[ + ["EfiSignatures", 0, [ + ["__tmp", 0, "uint32"], + ["__Signatures", 0, "Union", { + "selector": "x=>x.__tmp < 257", + "choices": { + "true": "EfiSignaturesListAttrib", + "false": "EfiSignaturesList" + } + }], + ["Signatures", 0, "Value", {"value": "x=>x.__Signatures.Signatures"}] + ]], + ["EfiSignaturesListAttrib", 0, [ + ["__Attributes", 0, "uint32"], + ["Signatures", 4, "Array", {"type": "Signature", "count": 1000}] + ]], + ["EfiSignaturesList", 0, [ + ["Signatures", 0, "Array", {"type": "Signature", "count": 1000}] + ]], + ["Signature", "x=>x.__ListSize", [ + ["__Type", 0, "GUID"], + ["Type", 0, "Value", {"value": "x=>x.__Type.Value"}], + ["__ListSize", 16, "uint32"], + ["__HeaderSize", 20, "uint32"], + ["Payload", 24, "Union", { + "selector": "x=>x.Type", + "choices": { + "{a5c059a1-94e4-4aa7-87b5-ab155c2bf072}": "Cert", + "{c1c41626-504c-4092-aca9-41f936934328}": "HashList" + } + }] + ]], + ["Cert", "x=>x.__SignatureSize + 4", [ + ["__SignatureSize", 0, "uint32"], + ["__Owner", 4, "GUID"], + ["Owner", 0, "Value", {"value": "x=>x.__Owner.Value"}], + ["__Data", 20, "String", {"length": "x=>x.__SignatureSize - 16", "term": "", "max_length": 10000}], + ["Cert", 0, "Value", {"value": "x=>parse_x509(data=x.__Data)[0]"}] + ]], + ["HashList", 0, [ + ["__SignatureSize", 0, "uint32"], + ["Hashes", 4, "Array", {"type": "Hash", "count": 1000, "sentinel": "x=>x.Owner = '{00000000-0000-0000-0000-000000000000}'"}] + ]], + ["Hash", 48, [ + ["__Owner", 0, "GUID"], + ["Owner", 0, "Value", {"value": "x=>x.__Owner.Value"}], + ["__Data", 16, "String", {"length": 32, "term": ""}], + ["Hash", 0, "Value", {"value": "x=>format(format='%048x', args=[x.__Data])"}] + ]], + ["GUID", 16, [ + ["__D1", 0, "uint32"], + ["__D2", 4, "uint16"], + ["__D3", 6, "uint16"], + ["__D4", 8, "String", {"term": "", "length": 2}], + ["__D5", 10, "String", {"term": "", "length": 6}], + ["Value", 0, "Value", { + "value": "x=>format(format='{%08x-%04x-%04x-%02x-%02x}', args=[x.__D1, x.__D2, x.__D3, x.__D4, x.__D5])" + }] + ]] + ]''' + + LET GetSignatures(Namespace, Name) = Select Name as Name, + parse_binary(accessor="data", filename=Value, + profile=PROFILE, struct="EfiSignatures").Signatures as Signatures + FROM efivariables(namespace=Namespace, name=Name, value=True) + +sources: + - name: Certificates + query: | + LET PK = Select * FROM foreach( + row=GetSignatures(Namespace="{8be4df61-93ca-11d2-aa0d-00e098032b8c}", Name="PK"), + query={ + Select * From foreach( + row=Signatures, + query={ + Select Name, Owner, Cert as Certificate From Payload + }) + }) + LET DB = Select * FROM foreach( + row=GetSignatures(Namespace="{d719b2cb-3d3a-4596-a3bc-dad00e67656f}", Name="db"), + query={ + Select * From foreach( + row=Signatures, + query={ + Select Name, Owner, Cert as Certificate From Payload + }) + }) + + Select * from chain( + a={ Select * From PK }, + b={ Select * From DB }) + + - name: Hashes + query: | + Select * FROM foreach( + row=GetSignatures(Namespace="{d719b2cb-3d3a-4596-a3bc-dad00e67656f}", Name="dbx"), + query={ + Select * From foreach( + row=Signatures, + query={ + Select * FROM foreach( + row=Payload.Hashes, + query={ + Select Name, Owner, Hash From scope() + }) + }) + }) diff --git a/artifacts/definitions/Generic/System/HostsFile.yaml b/artifacts/definitions/Generic/System/HostsFile.yaml new file mode 100755 index 000000000..803e9cf49 --- /dev/null +++ b/artifacts/definitions/Generic/System/HostsFile.yaml @@ -0,0 +1,93 @@ +name: Generic.System.HostsFile +description: | + The system hosts file maps hostnames to IP addresses. In some cases, + entries in this file take precedence and overrides the results from + the system DNS service. + + The file is a simple text file, with one line per IP address. Each + whitespace-separated word following the IP address is a hostname. + The Linux man page refers to the the first hostname as *canonical_hostname*, + and any following words as *aliases*. They are treated the same by this + artifact. + + The hosts file is typically present on all Linux-based systems (including macOS), + with entries for localhost. The same file format is also supported on Windows. + + The source *Hosts* returns each line in each hosts file that matches + the glob parameters for address and hostname. The hostname and aliases + are combined in a single column *Hostnames*. Columns returned: + + - OSPath + - Hostnames + - Comment + + Only comments that follows the hostname on the same line are captured in Comment. + Comments on their own lines are ignored. + + A second source *HostsFlattened* provides a flattened result, with each row + containing an IP address and a single hostname. + + This artifact also exports a function `parse_hostsfile()` that returns Hostname + and Aliases individually. + +reference: + - https://manpages.debian.org/bookworm/manpages/hosts.5.en.html + +export: | + LET _parse_hostsfile(OSPath) = SELECT parse_string_with_regex( + string=Line, + regex='''^[\t ]*(?P
[^\s#]+)[\t ]+(?P[^\s#]+)(?P[^#\n\r]+)?(?:[\t ]*#(?P.+))?''') AS Parsed + FROM parse_lines(filename=OSPath) + WHERE Parsed.Address + + LET parse_hostsfile(OSPath) = SELECT Parsed.Address AS Address, + Parsed.Hostname AS Hostname, + filter(list=split(sep='''\s+''', string=Parsed.Aliases), regex='.+') AS Aliases, + + /* Remove any whitespace between comment character and comment: */ + regex_replace(re='''^\s+''', source=Parsed.Comment, replace='$1') AS Comment + FROM _parse_hostsfile(OSPath=OSPath) + + LET Files = SELECT OSPath FROM glob(globs=hostsFileGlobs.HostsFileGlobs) + + LET HostsFiles = SELECT * FROM foreach(row=Files, query={ + SELECT OSPath, Address, Hostname, Aliases, Comment + FROM parse_hostsfile(OSPath=OSPath) + }) + +parameters: + - name: hostsFileGlobs + description: Globs to find hosts files + type: csv + default: | + HostsFileGlobs + C:\Windows\System32\drivers\etc\hosts + /etc/hosts + - name: HostnameRegex + description: Hostname or aliases to match + default: . + type: regex + - name: AddressRegex + description: IP addresses to match + default: . + type: regex + +sources: + - name: Hosts + query: | + SELECT OSPath, Address, + (Hostname, ) + Aliases AS Hostname, + Comment + FROM HostsFiles + WHERE Hostname =~ HostnameRegex + AND Address =~ AddressRegex + + - name: HostsFlattened + query: | + SELECT OSPath, Address, Hostname, Comment + FROM flatten(query={ + SELECT OSPath, Address, (Hostname, ) + Aliases AS Hostname, Comment + FROM HostsFiles + }) + WHERE Address =~ AddressRegex + AND Hostname =~ HostnameRegex diff --git a/artifacts/definitions/Generic/System/ProcessSiblings.yaml b/artifacts/definitions/Generic/System/ProcessSiblings.yaml new file mode 100644 index 000000000..e2195588d --- /dev/null +++ b/artifacts/definitions/Generic/System/ProcessSiblings.yaml @@ -0,0 +1,51 @@ +name: Generic.System.ProcessSiblings +description: | + This artifact queries the process tracker to display all known + sibling processes of the target process (i.e. all other processes + from the same parent). + + This is useful to reveal the complete interaction that included + the process in question (e.g. previous shell commands etc). + + Minimum Version: 0.6.6 + +parameters: + - name: CommandlineRegex + default: . + description: Target process by this command line + type: regex + + - name: PidFilter + description: Filter pids by this regex + default: . + type: regex + + - name: IncludePstree + type: bool + +sources: + - query: | + LET GetDetails(Records) = SELECT + Id AS ChildPid, + Data.CommandLine AS CommandLine, + Data.Username AS Username, + StartTime, EndTime + FROM Records + ORDER BY StartTime + + SELECT * FROM foreach(row={ + SELECT Pid, Ppid, Name + FROM process_tracker_pslist() + WHERE CommandLine =~ CommandlineRegex + AND Pid =~ PidFilter + }, query={ + SELECT Pid,Ppid, Name, ChildPid, + CommandLine, Username, StartTime, EndTime, + if(condition=IncludePstree, then=process_tracker_tree(id=Ppid)) AS ParentTree + FROM foreach(row=GetDetails( + Records=process_tracker_children(id=Ppid))) + }) + +column_types: + - name: ParentTree + type: tree diff --git a/artifacts/definitions/Generic/System/Pstree.yaml b/artifacts/definitions/Generic/System/Pstree.yaml index 1f341c1f7..8a04b55e7 100644 --- a/artifacts/definitions/Generic/System/Pstree.yaml +++ b/artifacts/definitions/Generic/System/Pstree.yaml @@ -4,12 +4,17 @@ description: | system by traversing the process's parent ID. It is useful for establishing where a process came from - for - example, if a powershell process is spawned from Winword (event via - a number of intemediary processes) it could mean word was + example, if a PowerShell process is spawned from Winword (event via + several intermediary processes) it could mean word was compromised. + A more accurate call chain will be available when the + Windows.Events.TrackProcesses artifact is collected (required + Sysmon) or Windows.Events.TrackProcessesBasic (does not require + Sysmon) + parameters: - - name: ProcessNameRegex + - name: CommandlineRegex default: . type: regex @@ -23,56 +28,21 @@ parameters: type: regex - name: CallChainSep - default: " <- " + default: " -> " + + - name: IncludePstree + type: bool sources: - query: | - // Cache the process listing in memory as we will be going - // through it many times. - LET processes <= SELECT Name, Pid, Ppid FROM pslist() WHERE Pid > 0 - - LET m <= memoize(query={ SELECT Name, Pid, Ppid FROM pslist()}, key="Pid", period=1) - - // Recursive function to find process parent (clients > 0.4.9). - // Catch recursion loops by limiting the depth of traversal to 10 deep. - LET pstree_memoized(LookupPid, Depth) = SELECT * FROM foreach( - row=if(condition=Depth < 10, then=get(item=m, field=LookupPid)), - query={ - SELECT * FROM chain( - a={ - SELECT Name, Pid, Ppid FROM scope() - }, - b={ - SELECT Name, Pid, Ppid FROM pstree_memoized( - LookupPid=Ppid, Depth=Depth + 1) - }) - }) - - // Recursive function to find process parent (clients < 0.4.9). - LET pstree_legacy(LookupPid) = SELECT * FROM foreach( - row={ - SELECT Name, Pid, Ppid FROM processes - WHERE Pid = LookupPid - }, - query={ - SELECT * FROM chain( - b={ - SELECT Name, Pid, Ppid FROM pstree(LookupPid=Ppid) - }, - a={ - SELECT Name, Pid, Ppid FROM scope() - }) - }) - - // Select which version we should use - LET pstree(LookupPid) = SELECT * FROM if( - condition=version(function="memoize") >= 0, - then={ SELECT * FROM pstree_memoized(LookupPid=LookupPid, Depth=0) }, - else={ SELECT * FROM pstree_legacy}) - - SELECT Name, Pid, Ppid, - join(array=pstree(LookupPid=Pid).Name, sep=CallChainSep) AS CallChain - FROM processes - WHERE Name =~ ProcessNameRegex + SELECT Pid, Ppid, Name, Username, Exe, CommandLine, StartTime, EndTime, + join(array=process_tracker_callchain(id=Pid).Data.Name, sep=CallChainSep) AS CallChain, + if(condition=IncludePstree, then=process_tracker_tree(id=Pid)) AS PSTree + FROM process_tracker_pslist() + WHERE CommandLine =~ CommandlineRegex AND CallChain =~ CallChainFilter - AND str(str=Pid) =~ str(str=PidFilter) + AND Pid =~ PidFilter + +column_types: + - name: PSTree + type: tree diff --git a/artifacts/definitions/Generic/Utils/DeadDiskRemapping.yaml b/artifacts/definitions/Generic/Utils/DeadDiskRemapping.yaml new file mode 100644 index 000000000..437c0bcd3 --- /dev/null +++ b/artifacts/definitions/Generic/Utils/DeadDiskRemapping.yaml @@ -0,0 +1,304 @@ +name: Generic.Utils.DeadDiskRemapping +description: | + Calculate a remapping configuration from a dead disk image. + + The artifact uses some heuristics to calculate a suitable remapping + configuration for a dead disk image: + + The following cases are handled: + + * If ImagePath is a directory to a mounted partition then we + generate directory remapping. This is suitable for handling images + with filesystems that Velociraptor cannot yet directly handle. + + * If the ImagePath points to a file which starts with the NTFS + signature we assume this is a partition image and not a disk + image. + + * If the ImagePath is a full disk image we assume it has a partition + table at the front, we then enumerate all the partitions and look + for an NTFS partition with a `Windows` directory at the top + level. We assume this is the windows drive and remap it to the C: + drive. + +type: SERVER + +parameters: + - name: ImagePath + default: /tmp/image.dd + description: Path to the image file to inspect. + + - name: Accessor + description: | + Accessor to read the image with. + + If not provided guess based on image file extension. + + - name: Hostname + default: Virtual Host + + - name: Upload + type: bool + default: "Y" + description: If specified we upload the generated YAML + + - name: CommonRemapping + description: Common clauses for all remapping in YAML + default: | + remappings: + - type: permissions + permissions: + - COLLECT_CLIENT + - FILESYSTEM_READ + - FILESYSTEM_WRITE + - READ_RESULTS + - MACHINE_STATE + - SERVER_ADMIN + - COLLECT_SERVER + - EXECVE + - type: impersonation + os: windows + hostname: {{ .Hostname }} + env: + - key: SystemRoot + value: C:\Windows + - key: WinDir + value: C:\Windows + disabled_functions: + - amsi + - lookupSID + - token + disabled_plugins: + - execve + - http_client + - users + - certificates + - handles + - pslist + - interfaces + - modules + - netstat + - partitions + - proc_dump + - proc_yara + - vad + - winobj + - wmi + - type: shadow + from: + accessor: zip + "on": + accessor: zip + - type: shadow + from: + accessor: raw_reg + "on": + accessor: raw_reg + - type: shadow + from: + accessor: raw_reg + "on": + accessor: raw_registry + - type: shadow + from: + accessor: raw_ntfs + "on": + accessor: ntfs + - type: shadow + from: + accessor: data + "on": + accessor: data + +export: | + -- Searches for a partition with a Windows directory, Unless this + -- is a partition image. + LET _FindWindowsPartition(ImagePath, Accessor) = SELECT * + FROM switch( + a={ + SELECT 0 AS StartOffset, Accessor, ImagePath AS PartitionPath + FROM stat(filename=ImagePath) + WHERE IsDir + }, + b={ + // Check for a partition image if there is an NTFS header at + // the start. + SELECT 0 AS StartOffset, + GuessAccessor(ImagePath=ImagePath) AS Accessor, + pathspec( + DelegateAccessor="offset", + Delegate=pathspec( + DelegateAccessor=GuessAccessor(ImagePath=ImagePath), + DelegatePath=ImagePath, + Path="0")) AS PartitionPath, + read_file(accessor=GuessAccessor(ImagePath=ImagePath), + filename=ImagePath, + length=4, offset=3) AS Magic + FROM scope() + WHERE Magic = "NTFS" + AND log(message="Detected NTFS signature at offset 0 - " + + "assuming this is a Windows partition image") + }, + c={ + + // Assume this is a disk image with a partition table - look + // for the first Windows OS partition + SELECT StartOffset, Accessor, _PartitionPath AS PartitionPath + FROM Artifact.Windows.Forensics.PartitionTable( + ImagePath=ImagePath, + Accessor=GuessAccessor(ImagePath=ImagePath)) + WHERE log(level="DEBUG", dedup=-1, + message="Searching for Windows directory: %#x-%#x (%v) %v - Magic %v", + args=[StartOffset, EndOffset, Size, name, Magic]) + AND TopLevelDirectory =~ "Windows" + AND log(message="Found Windows Partition at offset %#x with top level directory %v", + args=[StartOffset, TopLevelDirectory]) + LIMIT 1 + }) + + -- Guess the correct accessor based on the file extension. This + -- allows us to handle several image formats. + LET GuessAccessor(ImagePath) = Accessor || + if(condition=ImagePath =~ 'vmdk$', then='vmdk') || + if(condition=ImagePath =~ 'vhdx$', then='vhdx') || + if(condition=ImagePath =~ 'e01$', then='ewf') + + LET _MapHiveToKey(Hive, Key, Name, ImagePath) = log(dedup=-1, + message="Adding hive %v", args=Hive) && + dict(type="mount", + `description`=Name, + `from`=dict(accessor="raw_reg", + path_type="registry", + prefix=pathspec( + Path="/", + DelegateAccessor="raw_ntfs", + Delegate=ImagePath + Hive)), + on=dict(accessor="registry", prefix=Key, path_type="registry")) + + LET _MapDirHiveToKey(Hive, Key, Name) = log(dedup=-1, + message="Adding hive %v", args=Hive) && + dict(type="mount", + `description`=Name, + `from`=dict(accessor="raw_reg", + path_type="registry", + prefix=pathspec( + Path="/", + DelegateAccessor="file", + DelegatePath=Hive)), + on=dict(accessor="registry", prefix=Key, path_type="registry")) + + -- Look for user hives and map them in HKEY_USERS + LET _FindUserHives(ImagePath) = SELECT _MapHiveToKey( + Name="Map User hive for " + OSPath[-2], + Hive=OSPath, + Key="HKEY_USERS\\" + OSPath[-2], + ImagePath=ImagePath + ) AS Map + FROM glob(globs='/Users/*/NTUser.DAT', + accessor="raw_ntfs", + root=ImagePath) + WHERE log(dedup=-1, message="Found User Hive at %v", args=OSPath.Path) + + LET _FindDirUserHives(ImagePath) = SELECT _MapDirHiveToKey( + Name="Map User hive for " + OSPath[-2], + Hive=OSPath, + Key="HKEY_USERS\\" + OSPath[-2]) AS Map + FROM glob(globs='/Users/*/NTUser.DAT', + root=ImagePath) + WHERE log(dedup=-1, message="Found User Hive at %v", args=OSPath.Path) + + LET CalculateWindowsMappings(ImagePath) = Remappings.remappings + ( + dict(type="mount", + `from`=dict(accessor="raw_ntfs", prefix=ImagePath), + on=dict(accessor="ntfs", prefix="\\\\.\\C:", path_type="ntfs") + ), + dict(type="mount", + `from`=dict(accessor="raw_ntfs", prefix=ImagePath), + on=dict(accessor="file", prefix="C:", path_type="windows") + ), + dict(type="mount", + `from`=dict(accessor="raw_ntfs", prefix=ImagePath), + on=dict(accessor="auto", prefix="C:", path_type="windows") + ), + _MapHiveToKey(Name="Map Software Hive", + ImagePath=ImagePath, + Hive="/Windows/System32/Config/SOFTWARE", + Key="HKEY_LOCAL_MACHINE/Software"), + _MapHiveToKey(Name="Map Security Hive", + ImagePath=ImagePath, + Hive="/Windows/System32/Config/Security", + Key="HKEY_LOCAL_MACHINE/Security"), + _MapHiveToKey(Name="Map System Hive", + ImagePath=ImagePath, + Hive="/Windows/System32/Config/System", + Key="HKEY_LOCAL_MACHINE/System"), + _MapHiveToKey(Name="Map SAM Hive", + ImagePath=ImagePath, + Hive="/Windows/System32/Config/SAM", + Key="SAM"), + _MapHiveToKey(Name="Map Amcache Hive", + ImagePath=ImagePath, + Hive="/Windows/appcompat/Programs/Amcache.hve", + Key="Amcache") + ) + _FindUserHives(ImagePath=WindowsPartition.PartitionPath).Map + + LET CalculateWindowsDirMappings(ImagePath) = Remappings.remappings + ( + dict(type="mount", + description="Mount Directory " + ImagePath + " on C: drive", + `from`=dict(accessor="file", prefix=ImagePath), + on=dict(accessor="ntfs", prefix="\\\\.\\C:", path_type="ntfs") + ), + dict(type="mount", + `from`=dict(accessor="file", prefix=ImagePath), + on=dict(accessor="file", prefix="C:", path_type="windows") + ), + dict(type="mount", + `from`=dict(accessor="file", prefix=ImagePath), + on=dict(accessor="auto", prefix="C:", path_type="windows") + ), + _MapDirHiveToKey(Name="Map Software Hive", + Hive="/Windows/System32/Config/SOFTWARE", + Key="HKEY_LOCAL_MACHINE/Software"), + _MapDirHiveToKey(Name="Map Security Hive", + Hive="/Windows/System32/Config/Security", + Key="HKEY_LOCAL_MACHINE/Security"), + _MapDirHiveToKey(Name="Map System Hive", + Hive="/Windows/System32/Config/System", + Key="HKEY_LOCAL_MACHINE/System"), + _MapDirHiveToKey(Name="Map SAM Hive", + Hive="/Windows/System32/Config/SAM", + Key="SAM"), + _MapDirHiveToKey(Name="Map Amcache Hive", + Hive="/Windows/appcompat/Programs/Amcache.hve", + Key="Amcache") + ) + _FindDirUserHives(ImagePath=ImagePath).Map + +sources: +- query: | + LET WindowsPartition <= + _FindWindowsPartition(ImagePath=ImagePath, Accessor=Accessor)[0] + + LET Remappings <= parse_yaml( + filename=template(template=CommonRemapping, + expansion=dict(Hostname=Hostname)), + accessor="data") + + -- Select the type of mapping to calculate depending on what ImagePath is. + LET CalculateMappings = + ( stat(filename=ImagePath).IsDir && + CalculateWindowsDirMappings(ImagePath=ImagePath) ) || + ( WindowsPartition.PartitionPath && + CalculateWindowsMappings(ImagePath=WindowsPartition.PartitionPath) ) || + log(message="No suitable mapping found") + + LET YamlText = serialize(format="yaml", + item=dict(remappings=CalculateMappings)) + + SELECT if(condition=Upload, + then=upload(accessor="data", file=YamlText, name="remapping.yaml"), + else=YamlText) AS Remapping + FROM scope() + +column_types: +- name: Remapping + type: upload_preview diff --git a/artifacts/definitions/Generic/Utils/FetchBinary.yaml b/artifacts/definitions/Generic/Utils/FetchBinary.yaml index e6a11ca81..e3fe26467 100644 --- a/artifacts/definitions/Generic/Utils/FetchBinary.yaml +++ b/artifacts/definitions/Generic/Utils/FetchBinary.yaml @@ -5,7 +5,7 @@ description: | from the source URL. This artifact is designed to be called from other artifacts. The - binary path will be emitted in the FullPath column. + binary path will be emitted in the OSPath column. As a result of launching an artifact with declared "tools" field, the server will populate the following environment @@ -13,7 +13,11 @@ description: | Tool__HASH - The hash of the binary Tool__FILENAME - The filename to store it. - Tool__URL - The URL. + Tool__URL - The URL to fetch the binary from. + Tool__URLs - A set of possible URLs to fetch the binary from. + + Older server versions only supported a single URL but current + versions send a set of URLs to try in order. parameters: - name: ToolName @@ -31,7 +35,7 @@ parameters: - name: ToolInfo type: hidden - description: A dict containing the tool information. + description: A dict containing the tool information (deprecated). - name: TemporaryOnly type: bool @@ -39,111 +43,121 @@ parameters: If true we use a temporary directory to hold the binary and remove it afterwards + - name: IgnoreErrors + type: bool + description: If set we ignore errors and let the caller handle it. + +implied_permissions: + - SERVER_ADMIN + - FILESYSTEM_WRITE + - NETWORK + sources: - query: | - -- The following VQL is particularly ancient because it is - -- running on the client and it needs to be compatibile with - -- clients at least back to 0.3.9 - - LET info_cache <= SELECT * FROM info() - LET inventory_item = SELECT inventory_get(tool=ToolName) AS Item FROM scope() - - LET args <= SELECT * FROM switch( - // Try to get info from the ToolInfo parameter. - a={SELECT get(field="Tool_" + ToolName + "_HASH", item=ToolInfo) AS ToolHash, - get(field="Tool_" + ToolName + "_FILENAME", item=ToolInfo) AS ToolFilename, - get(field="Tool_" + ToolName + "_URL", item=ToolInfo) AS ToolURL, - get(field="Tool_" + ToolName + "_PATH", item=ToolInfo) AS ToolPath - FROM scope() WHERE ToolFilename}, - - // Failing this - get it from the scope() - b={SELECT get(field="Tool_" + ToolName + "_HASH", item=scope()) AS ToolHash, - get(field="Tool_" + ToolName + "_FILENAME", item=scope()) AS ToolFilename, - get(field="Tool_" + ToolName + "_URL", item=scope()) AS ToolURL, - get(field="Tool_" + ToolName + "_PATH", item=ToolInfo) AS ToolPath - FROM scope() WHERE ToolFilename}, - - // Failing this - try to get it from the inventory service directly. - c={SELECT get(field="Tool_" + ToolName + "_HASH", item=(inventory_item[0]).Item) AS ToolHash, - get(field="Tool_" + ToolName + "_FILENAME", item=(inventory_item[0]).Item) AS ToolFilename, - get(field="Tool_" + ToolName + "_URL", item=(inventory_item[0]).Item) AS ToolURL - FROM scope() WHERE ToolFilename} - ) - - // Keep the binaries cached in the temp directory. We verify the - // hashes all the time so this should be safe. - LET binpath <= SELECT Path FROM switch( - - -- Allow user to specify a temporary directory which - -- will be cleaned up. - a={SELECT tempdir(remove_last=TRUE) AS Path - FROM scope() WHERE TemporaryOnly }, - - -- Otherwise use the temp directory (The official MSI - -- sets this to a known location) - b={SELECT dirname(path=tempfile()) AS Path - FROM scope() WHERE Path }, - - c={SELECT "/tmp" AS Path FROM info_cache WHERE OS = "linux" } - ) - - // Where we should save the file. - LET ToolPath <= SELECT path_join(components=[ - (binpath[0]).Path, (args[0]).ToolFilename]) AS Path FROM scope() - - // Support tools locally served from disk - LET local_file = - SELECT hash(path=(args[0]).ToolPath) as Hash, - (args[0]).ToolFilename AS Name, - "Downloaded" AS DownloadStatus, - (args[0]).ToolPath AS FullPath - FROM scope() - WHERE (args[0]).ToolPath AND - log(message="File served from " + (args[0]).ToolPath) + LET S = scope() + + -- 1GB max + LET HASH_MAX_SIZE <= S.HASH_MAX_SIZE || 1000000000 + + -- Optionally accepts multiple download URLs from the server + LET ParseUrls(Url) = parse_json_array(data=Url || '[]') + + LET args <= dict( + ToolHash=get(field="Tool_" + ToolName + "_HASH"), + ToolFilename=get(field="Tool_" + ToolName + "_FILENAME"), + ToolURL=get(field="Tool_" + ToolName + "_URL"), + ToolURLs=ParseUrls(Url=get(field="Tool_" + ToolName + "_URLs"))) + + LET _ <= if(condition=NOT args.ToolFilename, + then=log(level="ERROR", + message="Tool %v not configured by the server. Did you define it as an artifact tool? %v", args=[ToolName, args])) + + // By default the temp directory is created inside a trusted directory. + LET TempDir <= tempdir(remove_last=TRUE) + + // Where to store the file. If the user specified TemporaryOnly we + // remove it with the tempdir, otherwise we store it in the trusted + // directory. + LET binpath <= if(condition=TemporaryOnly, then=TempDir, else=dirname(path=TempDir)) + + // Where we should save the file - use the filename as specified by the server. + LET ToolPath <= path_join(components=[binpath, args.ToolFilename || "Unknown"]) // Download the file from the binary URL and store in the local // binary cache. - LET download = SELECT * FROM if(condition=log( - message="URL for " + (args[0]).ToolFilename + - " is at " + (args[0]).ToolURL + " and has hash of " + (args[0]).ToolHash) - AND binpath AND (args[0]).ToolHash AND (args[0]).ToolURL, + // If http_client support multiple URLs use them. + LET download_multiple = SELECT * FROM if(condition=args.ToolURLs + AND version(plugin="http_client") > 2 + AND log( + message="URLs for %v are at %v. The tool has a hash of %v", args=[ + args.ToolFilename , args.ToolURLs, args.ToolHash + ]) + AND args.ToolHash, + then={ + SELECT hash(path=Content) as Hash, + args.ToolFilename AS Name, + "Downloaded" AS DownloadStatus, + copy(filename=Content, dest=ToolPath, + permissions=if(condition=IsExecutable, then="x")) AS OSPath + FROM http_client(url=args.ToolURLs, tempfile_extension=".tmp") + WHERE log(message=format(format="downloaded hash of %v: %v, expected %v", args=[ + Content, Hash.SHA256, args.ToolHash])) + AND Hash.SHA256 = args.ToolHash + }) + + // Download the file from the binary URL and store in the local + // binary cache. Used for old clients with http_client that only supports one URL. + LET download_single = SELECT * FROM if(condition=log( + message="URL for " + args.ToolFilename + + " is at " + args.ToolURL + " and has hash of " + args.ToolHash) + AND args.ToolHash AND args.ToolURL, then={ SELECT hash(path=Content) as Hash, - (args[0]).ToolFilename AS Name, + args.ToolFilename AS Name, "Downloaded" AS DownloadStatus, - copy(filename=Content, dest=(ToolPath[0]).Path, - permissions=if(condition=IsExecutable, then="x")) AS FullPath - FROM http_client(url=(args[0]).ToolURL, tempfile_extension=".exe") + copy(filename=Content, dest=ToolPath, + permissions=if(condition=IsExecutable, then="x")) AS OSPath + FROM http_client(url=args.ToolURL, tempfile_extension=".tmp") WHERE log(message=format(format="downloaded hash of %v: %v, expected %v", args=[ - Content, Hash.SHA256, (args[0]).ToolHash])) - AND Hash.SHA256 = (args[0]).ToolHash + Content, Hash.SHA256, args.ToolHash])) + AND Hash.SHA256 = args.ToolHash }, else={ SELECT * FROM scope() - WHERE NOT log(message="No valid setup - is tool " + ToolName + - " configured in the server inventory?") + WHERE NOT log( + level="ERROR", message="No valid setup - is tool " + ToolName + + " configured in the server inventory?") }) // Check if the existing file in the binary file cache matches // the hash. - LET existing = SELECT FullPath, hash(path=FullPath) AS Hash, Name, + LET existing = SELECT OSPath, hash(path=OSPath) AS Hash, Name, "Cached" AS DownloadStatus - FROM stat(filename=(ToolPath[0]).Path) + FROM stat(filename=ToolPath) WHERE log(message=format(format="Local hash of %v: %v, expected %v", args=[ - FullPath, Hash.SHA256, (args[0]).ToolHash])) - AND Hash.SHA256 = (args[0]).ToolHash + OSPath, Hash.SHA256, args.ToolHash])) + AND Hash.SHA256 = args.ToolHash // Find the required_tool either in the local cache or // download it (and put it in the cache for next time). If we // have to download the file we sleep for a random time to // stagger server bandwidth load. - SELECT * FROM switch( - a=local_file, + SELECT *, OSPath AS FullPath + FROM switch( b=existing, c={ SELECT rand(range=SleepDuration) AS timeout FROM scope() - WHERE args AND (args[0]).ToolURL AND + WHERE args AND args.ToolURL AND log(message=format(format='Sleeping %v Seconds', args=[timeout])) AND sleep(time=timeout) AND FALSE }, - d=download) + d=download_multiple, + e=download_single, + f={ + -- Emit an error message to fail the collection. + SELECT * + FROM scope() + WHERE if(condition=NOT IgnoreErrors, + then=log(message="tool %v not available!", level="ERROR", args=ToolName)) + AND FALSE + }) diff --git a/artifacts/definitions/Generic/Utils/SendEmail.yaml b/artifacts/definitions/Generic/Utils/SendEmail.yaml new file mode 100644 index 000000000..72fa95ff2 --- /dev/null +++ b/artifacts/definitions/Generic/Utils/SendEmail.yaml @@ -0,0 +1,185 @@ +name: Generic.Utils.SendEmail +author: Andreas Misje – @misje +description: | + A Utility artifact for sending emails. + + This artifact handles the challenges of MIME, encodings and other pitfalls + of sending anything but simple plain-text emails. It will, among other things, + + - Let you provide both HTML and plain-text email bodies, letting the email + client pick either HTML or plain-text, depending on what it supports (utilising + "multipart/alternative") + - Text is encoded as Base64 (unless disabled), split into 76-character-wide + lines in order to conform with RFC standards + - Attachments are supported and automatically encoded + - The whole email is sent as a multi-part message + + All of the functions used to create the final body of the email are exported + and are available for further customisation when sending an email. + +type: SERVER + +parameters: +- name: Secret + description: The name of the secret to use to send the mail with. + +- name: Recipients + type: json_array + default: '["noone@example.org"]' + description: Where to send the mail to. + +- name: Sender + description: The sender address (from). + +- name: FilesToUpload + type: csv + description: Files to upload, optionally renamed. + default: | + Path,Filename + +- name: PlainTextMessage + description: A plain-text message. + +- name: HTMLMessage + description: An HTML-formatted message. + +- name: EncodeText + type: bool + default: true + description: | + Base64-encode plain-text and HTML. If disabled, ensure to keep lines within + 998 octets, or encode the data manually and include an encoding header. + +- name: Subject + default: A message from Velociraptor + +- name: Period + type: int + default: 10 + description: | + Refuse to send mails more often than this interval (in seconds). This throttling + is applied to the whole server. + +- name: UseSimpleBoundary + type: bool + description: | + Use a barrier consisting of only [A-Za-z0-9] characters. Some e-mail clients + do not support / conform to the RFC 2045 standard, and cannot handle + boundaries with characters other than simple lower- and upper-case letters, + as well as numbers (i.e not ['()+_,./:=?']). + +export: | + LET _RandomString = SELECT format(format="%c", args=20 + rand(range=107)) AS Ch + FROM range(end=1000) + WHERE Ch =~ if(condition=UseSimpleBoundary, + then="[A-Za-z0-9]", + else="[A-Za-z0-9'()+_,./:=?]") + LIMIT 70 + + -- Create a random string suitable as a MIME boundary: + LET RandomString = join(array=_RandomString.Ch) + + -- Base64-encode data and split the result into 76-character long lines (as per + -- RFC 2045 6.8). Note that a "Content-Transfer-Encoding: base64" header is + -- needed for this message to interpreted correctly: + LET EncodeData(Data) = regex_replace(re="(.{76})", + replace="$1\r\n", + source=base64encode(string=Data)) + + -- Wrap Sections in boundaries. Header may be used to create a sub-boundary, + -- useful for multipart/alternative: + LET WrapInBoundary(Boundary, Sections, Header="") = template( + template="{{ if .header }}{{ .header }}; boundary={{ .boundary }}\r\n\r\n{{ end }}{{ range .sections }}--{{ $.boundary }}\r\n{{ . }}{{ end }}--{{ $.boundary }}--\r\n", + expansion=dict( + boundary=Boundary, + sections=Sections, + header=Header)) + + -- Add content type ("plain" or "html") and newlines to text. If Encode is set, + -- encode the text in Base64 and add a suitable transfer header: + LET WrapText(Value, Type, Encode=false) = if( + condition=Value, + then=format( + format='Content-Type: text/%s; charset="utf-8"%s\r\n\r\n%v\r\n', + args=[Type, if(condition=Encode, + then="\r\nContent-Transfer-Encoding: base64", + else=""), if( + condition=Encode, + then=EncodeData(Data=Value), + else=Value)])) + + -- Wrap text (plain, HTML or both) in multipart/alternative, letting clients + -- pick either HTML or plain-text, depending on what they support. If just + -- one of Plain/HTML is specified, multipart/alternative is not used: + LET WrapAlternative(Plain, HTML) = if( + condition=Plain + AND HTML, + then=WrapInBoundary(Header="Content-Type: multipart/alternative", + Boundary=RandomString, + Sections=(Plain, HTML)), + else=Plain || HTML) + + -- Encodes the file as base64: + LET EncodeFile(Filename) = EncodeData(Data=read_file(filename=Filename)) + + -- A Helper function to embed a file content from disk. + LET AttachFile(Path, Filename) = template( + template='Content-Type: application/octet-stream; name="{{ .name }}"\r\nContent-Disposition: attachment; filename="{{ .filename }}"\r\nContent-Transfer-Encoding: base64\r\n\r\n{{ .data }}\r\n\r\n', + expansion=dict( + name=regex_replace( + source=basename( + path=Filename), + re='''\..+$''', + replace=''), + filename=basename( + path=Filename), + data=EncodeFile( + Filename=Path))) + + -- Call AttachFile() for each file in Files that exist. Files must be an array + -- of dicts with the members "Path" and an optional "Filename", which is used + -- to replace the attachment filename. Useful for temporary files: + LET AttachFiles(Files) = SELECT AttachFile(Path=Path, + Filename= + get(field='Filename', + default= + Path)) AS Part + FROM foreach(row=Files) + WHERE (stat(filename=Path).OSPath + AND log(message="Attaching %v", args=Path, dedup=-1, level='INFO')) OR NOT + log( + message="Fail to attach %v", + args=Path, + dedup=-1, + level='WARN') + +sources: +- query: | + LET Texts <= WrapAlternative(Plain=WrapText( + Value=PlainTextMessage, + Type='plain', + Encode=EncodeText), + HTML=WrapText(Value=HTMLMessage, + Type='html', + Encode=EncodeText)) + + LET Texts <= if(condition=Texts, then=[Texts], else=[]) + + LET Boundary <= RandomString + + LET Headers <= dict(`Content-Type`='multipart/mixed; boundary=' + Boundary) + + -- Build the email parts - first the text message, then the attachments. + LET Message <= WrapInBoundary( + Boundary=Boundary, + Sections=Texts + AttachFiles(Files=FilesToUpload).Part) + + -- Send the mail + SELECT mail(secret=Secret, + `to`=Recipients, + `from`=Sender, + period=Period, + subject=Subject, + headers=Headers, + `body`=Message) AS Mail + FROM scope() \ No newline at end of file diff --git a/artifacts/definitions/Linux/Applications/Chrome/Extensions.yaml b/artifacts/definitions/Linux/Applications/Chrome/Extensions.yaml index 670b9b0a2..25ca2df52 100644 --- a/artifacts/definitions/Linux/Applications/Chrome/Extensions.yaml +++ b/artifacts/definitions/Linux/Applications/Chrome/Extensions.yaml @@ -26,8 +26,10 @@ sources: SELECT Uid, User, Homedir from Artifact.Linux.Sys.Users() }, query={ - SELECT FullPath, Mtime, Ctime, User, Uid from glob( - globs=Homedir + '/' + extensionGlobs) + SELECT OSPath, Mtime, Ctime, User, Uid + FROM glob( + globs=extensionGlobs, + root=Homedir) }) /* If the Manifest declares a default_locale then we @@ -69,7 +71,7 @@ sources: SELECT Filename as ManifestFilename, Uid, User, parse_json(data=Data) as Manifest - FROM read_file(filenames=FullPath) + FROM read_file(filenames=OSPath) }, query=maybe_read_locale_file) diff --git a/artifacts/definitions/Linux/Applications/Chrome/Extensions/Upload.yaml b/artifacts/definitions/Linux/Applications/Chrome/Extensions/Upload.yaml index fc0b4d8bf..0539fa0d5 100644 --- a/artifacts/definitions/Linux/Applications/Chrome/Extensions/Upload.yaml +++ b/artifacts/definitions/Linux/Applications/Chrome/Extensions/Upload.yaml @@ -2,7 +2,7 @@ name: Linux.Applications.Chrome.Extensions.Upload description: | Upload all users chrome extension. - We dont bother actually parsing anything here, we just grab all the + We don't bother actually parsing anything here, we just grab all the extension files in user's home directory. parameters: @@ -19,7 +19,7 @@ sources: SELECT Uid, User, Homedir from Artifact.Linux.Sys.Users() }, query={ - SELECT FullPath, Mtime, Ctime, User, Uid, - upload(file=FullPath) as Upload - FROM glob(globs=Homedir + '/' + extensionGlobs) + SELECT OSPath, Mtime, Ctime, User, Uid, + upload(file=OSPath) as Upload + FROM glob(globs=extensionGlobs, root=Homedir) }) diff --git a/artifacts/definitions/Linux/Applications/Docker/Info.yaml b/artifacts/definitions/Linux/Applications/Docker/Info.yaml index 18528376a..1e7a9c6e4 100644 --- a/artifacts/definitions/Linux/Applications/Docker/Info.yaml +++ b/artifacts/definitions/Linux/Applications/Docker/Info.yaml @@ -5,6 +5,10 @@ parameters: description: | Docker server socket. You will normally need to be root to connect. default: /var/run/docker.sock + +implied_permissions: +- NETWORK + sources: - precondition: | SELECT OS From info() where OS = 'linux' diff --git a/artifacts/definitions/Linux/Applications/Docker/Version.yaml b/artifacts/definitions/Linux/Applications/Docker/Version.yaml index 528953ee0..46384cf3a 100644 --- a/artifacts/definitions/Linux/Applications/Docker/Version.yaml +++ b/artifacts/definitions/Linux/Applications/Docker/Version.yaml @@ -1,10 +1,15 @@ name: Linux.Applications.Docker.Version description: Get Dockers version by connecting to its socket. + parameters: - name: dockerSocket description: | Docker server socket. You will normally need to be root to connect. default: /var/run/docker.sock + +implied_permissions: +- NETWORK + sources: - precondition: | SELECT OS From info() where OS = 'linux' diff --git a/artifacts/definitions/Linux/Debian/AptSources.yaml b/artifacts/definitions/Linux/Debian/AptSources.yaml index 771d74747..b96af2947 100644 --- a/artifacts/definitions/Linux/Debian/AptSources.yaml +++ b/artifacts/definitions/Linux/Debian/AptSources.yaml @@ -2,85 +2,472 @@ name: Linux.Debian.AptSources description: | Parse Debian apt sources. - We first search for \*.list files which contain lines of the form + This Artifact searches for all apt sources files and parses all + fields in both one–line `*.list` files and `*.sources` files + (deb822-style format). The results are presented both in a readable + table and a flattened version for parsing. - .. code:: console + `*.list` files contains lines of the form - deb http://us.archive.ubuntu.com/ubuntu/ bionic main restricted + ``` + deb http://us.archive.ubuntu.com/ubuntu/ bionic main restricted + deb-src [arch=amd64,i386 signed-by=/usr/share/keyrings/foo.gpg] https://foo.bar.baz/ubuntu/main jammy main restricted universe multiverse # Comment + ``` - For each line we construct the cache file by spliting off the - section (last component) and replacing / and " " with _. + deb indicates a source for binary packages, and deb-src instructs APT where + to find source code for packages. - We then try to open the file. If the file exists we parse some - metadata from it. If not we leave those columns empty. + `*.sources` files (deb822-style format) are in the form of key–value + lines, and as opposed to the one–line format, they may contain + multiple URIs, components and types (deb/deb-src), along with + embedded GPG keys. Example: + + ``` + Types: deb deb-src + URIs: file:/home/apt/debian http://foo.bar.baz/main + Suites: unstable + Components: main contrib non-free + ``` + + The exported function `parse_aptsources(OSPath, flatten)` parses + both formats and returns an (optionally flattened) table with + + - OSPath + - Types (deb/deb-src) + - Components (e.g. main/contrib/non-free/restricted,universe) + - Suites (e.g. unstable/bookworm/jammy) + - _URIBase (.e.g us.archive.ubuntu.com/ubuntu/) + - _Transport (e.g. http/https/file/cdrom/ftp) + - URIs (e.g. http://us.archive.ubuntu.com/ubuntu/) + + Any option is added to an individual column. The most common options + are + + - Architectures (e.g. amd64/i386/armel) + - Signed-By (e.g. /usr/share/keyrings/osquery.gpg) + + All known option names are transformed to the plural PascalCase + variants as listed in the sources.list man page. Any undocumented + options will still be included in the results, with names unchanged. + Options in the one-line format of the form "lang+=de"/"arch-=i386" + will be put in columns like "Languages-Add"/"Architectures-Remove", + matching the option names having the same effect in deb822. + + Entries in deb822 sources files may be disabled by including + "Enabled: no" instead of commenting out all lines. If this field + is not present with a "false" value, the entry is enabled. Use the + exported functions DebTrue()/DebFalse() to correctly parse all + accepted true/false strings, or use the VQL suggestion "Only enabled + sources" to filter on this column (true), if present. + + If the GPG key is embedded in a .sources file, the whole GPG key + will be included in the cell. Otherwise the value will be a file + path. Use the VQL suggestion "Hide embedded GPG keys" to replace + embedded GPG keys with "(embedded)" in the results. To + inspect the keys themselves (files or embedded data), use the + exchange artifact Linux.Debian.GPGKeys. + + If the function parameter "flatten" is False, multi–value fields + (like Components) will be combined in a single space-separated + string in each row. + + In addition to the two apt sources tables, a third table correlates + information from InRelease and Release files to provide additional + metadata. The modification timestamps may tell when the package + lists where last updated. reference: - - https://osquery.io/schema/3.2.6#apt_sources + - https://manpages.debian.org/bookworm/apt/sources.list.5.en.html + - https://manpages.debian.org/bookworm/dpkg-dev/deb822.5.en.html + - https://salsa.debian.org/apt-team/apt/-/blob/main/apt-pkg/sourcelist.cc + - https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files + +export: | + /* Remove whitespace from the beginning and end of a string: */ + LET Trim(string) = regex_transform(source=string, map=dict( + `(?m)^\\s+`='', + `(?m)\\s+$`='' + )) + + /* Replace any repeating whitespace with a single space: */ + LET Simplify(string) = regex_replace(source=string, re='''\s+''', replace=' ') + + /* The syntax in lists (deb822) and sources (one-line) files varies a bit, + and deb822 is case-insensitive. Normalise all known fields (as per + the man page): */ + LET NormaliseOpts(string) = regex_transform(source=string, map=dict( + `(?i)types|type`='Types', + `(?i)uris|uri`='URIs', + `(?i)suites|suite`='Suites', + `(?i)components|component`='Components', + `(?i)architectures$|arch$`='Architectures', + `(?i)architectures-add`='Architectures-Add', + `(?i)architectures-remove`='Architectures-Remove', + `(?i)languages$|lang$`='Languages', + `(?i)languages-add`='Languages-Add', + `(?i)languages-remove`='Languages-Remove', + `(?i)targets$|target$`='Targets', + `(?i)targets-add`='Targets-Add', + `(?i)targets-remove`='Targets-Remove', + `(?i)pdiffs`='PDiffs', + `(?i)by-hash`='By-Hash', + `(?i)allow-insecure`='Allow-Insecure', + `(?i)allow-weak`='Allow-Weak', + `(?i)allow-downgrade-to-insecure`='Allow-Downgrade-To-Insecure', + `(?i)trusted`='Trusted', + `(?i)signed-by`='Signed-By', + `(?i)check-valid-until`='Check-Valid-Until', + `(?i)valid-until-min`='Valid-Until-Min', + `(?i)valid-until-max`='Valid-Until-Max', + `(?i)check-date`='Check-Date', + `(?i)date-max-future`='Date-Max-Future', + `(?i)inrelease-path`='InRelease-Path', + `(?i)enabled`='Enabled' + )) + + LET DebTrue(string) = if( + condition=string=~'(?i)^(?:yes|true|with|on|enable)$', + then=true, else=false) + LET DebFalse(string) = if( + condition=string=~'(?i)^(?:no|false|without|off|disable)$', + then=true, else=false) + + /* Extract Key–Value pairs from option string. If assignment is -=/+=, + the -/+ operator is captured in Op: */ + LET OptStringToKeyValues__(string) = SELECT * + FROM parse_records_with_regex( + regex='''(?P[^ ]+?)(?P-|\+)?=(?P[^ ]+)''', + accessor='data', file=string + ) + + /* Since option values may have multiple words, split them and flatten + the results for further processing: */ + LET OptStringToKeyValues_(string) = SELECT * + FROM flatten(query={ + SELECT Key, + Op, + split(sep_string=',', string=Value) AS Value + FROM OptStringToKeyValues__(string=string) + }) + + /* Since options may be repeated, enumerate and group all values + per key and operation: */ + LET OptStringToKeyValues(string) = SELECT Key, + Op, + enumerate(items=Value) AS Value + FROM OptStringToKeyValues_(string=string) + GROUP BY Key, Op + + /* When an option is specified with +/-, represent this by appending + -Add/-Remove to the option name. These names match the syntax in + the deb822 format (i.e. "arch-=i386" == "Arhitectures-Remove: i386"). + The purpose of these assignments is to keep the default values + (rather than overriding them), but add or remove one or several + values: */ + LET OpName(op) = if(condition=op='+',then='-Add',else= + if(condition=op='-',then='-Remove',else='')) + + /* Convert a string of key–value pairs to a dict, and use consistent + option names: */ + LET OptStringToDict(string, should_flatten) = to_dict(item={ + SELECT NormaliseOpts(string=Key)+OpName(op=Op) AS _key, + if(condition=should_flatten, then=Value, + else=join(array=Value, sep=' ')) AS _value + FROM OptStringToKeyValues(string=string) + }) + + /* Parse a one-line deb sources.list file with options as a single string: */ + LET DebOneLine_Opts(OSPath) = SELECT OSPath, Type AS Types, + Simplify(string=Options) AS Options, URI AS URIs, + Transport AS _Transport, URIBase AS _URIBase, Suite AS Suites, + Simplify(string=Trim(string=Components)) AS Components + FROM parse_records_with_regex( + file=OSPath, + /* This regex attemps to cover most of the ways a sources + line can be written without being overly complex. Quotes + ("" and []) are actually allowed to certain degree by the + apt source code, but this is considered obscure syntax and + is not expected to be found in the wild. The exception is + "cdrom:[word word…]", which is capture correctly in order + to not end up with incorrectly captured words: */ + regex='''(?m)^\s*(?Pdeb(-src)?)(?:\s+\[(?P[^\]#]+)(?:#[^\]]+)?\])?\s+"?(?P(?P[^:]+):(?://)?(?P\[.+?\]|\S+?))"?\s+(?P\S+)\s+(?P[^\n#]+)''' + ) + + /* Parse a one-line deb sources.list file and output a dict: */ + LET DebOneLine_Dict(OSPath, should_flatten) = SELECT OSPath, * + FROM foreach(row=DebOneLine_Opts(OSPath=OSPath), + query={SELECT _value + + OptStringToDict(string=Options, + should_flatten=should_flatten) AS Contents + FROM items(item={SELECT Types, URIs, _Transport, _URIBase, Suites, + if(condition=should_flatten, then=split(sep_string=' ', + string=Components), else=Components) AS Components + FROM scope() + }) + }) + + /* Parse a one-line deb sources.list file with options in individual columns: */ + LET DebOneLine(OSPath) = SELECT OSPath, * FROM foreach( + row=DebOneLine_Dict(OSPath=OSPath, should_flatten=false), + column='Contents' + ) + + /* Parse a one-line deb sources.list file with options in individual + columns and flatten: */ + LET DebOneLine_Flattened(OSPath) = SELECT OSPath, * FROM flatten( + query={SELECT * FROM foreach( + row=DebOneLine_Dict(OSPath=OSPath, should_flatten=true), + column='Contents' + ) + }) + + /* Extract the transport/protocol and base from a URI: */ + LET URIComponents(URI) = parse_string_with_regex( + regex='''(?P[^:]+):(?://)?(?P[^\s]+)''', + string=URI + ) + + /* Although the documentation says to use whitespace and not comma + for multi-values in deb822, comma still appears to be supported, + and this use is seen in the wild. Treat these values correctly. + Note that this does not affect all keys, like suites and + components: + */ + LET MaybeReplaceComma(key, value) = if( + condition=key=~'(?i)^(?:arch|lang|targets)', + then=regex_replace(re='\s*,\s*', source=value, replace=' '), + else=value) + + /* Parse a deb822 sources file section into a series of key–value pairs. + Notes about the format: + - Keys must be at the beginning of the line (no whitespace allowed) + - Keys are case-insensitive + - Keys may be repeated. Values are not overridden, but combined + - Special keys that end in -Add/-Remove uses the default values, + but add or remove individual values. These keys are treated as + individual option names. + - Comments may only appear at the beginning of the line + - Multiple values are separated by whitespace, not comma. However, + some multi-value fields separated by comma are still split, even + if this is not mentioned in the documentation. + - Values may be multi-line (like when containing an embedded GPG key), + but following lines must be prefixed by whitespace. Multilines + may contain comments (prefixed by whitespace or not). Empty lines + part of a multi-line value must be prefixed by whitespace and "." + - A file may contain multiple entries, separated by empty lines. + A file must be split into sections, fed individually to this function + */ + LET Deb822_KeyValues___(section) = SELECT Key, + /* Signed-By is special (it could be an embedded GPG key),and + shouldn't be split: */ + if(condition=NormaliseOpts(string=Key)!='Signed-By', + then=split(sep_string=' ', + string=MaybeReplaceComma(key=Key, + value=Simplify(string=Trim(string=Value)))), + else=Value) AS Value + FROM parse_records_with_regex( + accessor='data', + /* A key is anything but whitespace up to a colon + Values can continue on several lines, but only if the following + lines are indented with whitespace + */ + regex='''(?m)^(?P[^#:\s]+)\s*:[^\S\n]*(?P[^\n]*(?:\n[^\S\n]+[^\n]+)*)''', + /* Before parsing the key–values, remove all comments from the file + (otherwise forming a regex without lookarounds would be very + difficult, if not impossible), Luckily, comments follow strict + rules and must start with ^#. + */ + file=regex_replace( + re='''(?m)^#.+\n''', + source=section + ) + ) + + LET Deb822_KeyValues__(section) = SELECT * FROM flatten(query={ + SELECT * FROM Deb822_KeyValues___(section=section) + }) + + LET Deb822_KeyValues_(section) = SELECT Key, + enumerate(items=Value) AS Value + FROM Deb822_KeyValues__(section=section) + GROUP BY Key + + /* Parse a deb822 sources file section into a dict with consistent option + names: */ + LET Deb822_KeyValues(section, should_flatten) = SELECT to_dict( + item={ + SELECT NormaliseOpts(string=Key) as _key, + if(condition=should_flatten, then=Value, + else=join(array=Value, sep=' ')) AS _value + FROM Deb822_KeyValues_(section=section) + }) AS Contents + FROM scope() + + /* Split paragraphs in a file (separated by one or several empty + lines) into rows. ('regex' is just anything that is illegal in Deb822Sections + to prevent splitting data into records.): */ + LET Deb822Sections(OSPath) = SELECT OSPath,* FROM split_records( + filenames=OSPath, + columns='Section', + regex='^ #', record_regex='''\n{2,}''' + ) + + LET Deb822_Flattened_(OSPath) = SELECT * FROM foreach( + row=Deb822Sections(OSPath=OSPath), + query={SELECT OSPath, * FROM flatten(query={ + SELECT * FROM foreach( + row=Deb822_KeyValues(section=Section, should_flatten=true), + column='Contents' + ) + })} + ) + /* DEB822_Sections() may produce empty rows. Exclude these by filtering + for a required column, like URIs: */ + WHERE URIs + + /* Parse a deb822 sources file with options in individual columns. + Note that, as opposed to DebOneLine and Deb822_Flattened, this + function does not return the columns _URIBase and _Transport, since + this format supports mulitple URIs to be specified: */ + LET Deb822(OSPath) = SELECT * FROM foreach( + row=Deb822Sections(OSPath=OSPath), + query={SELECT OSPath, * FROM foreach( + row=Deb822_KeyValues(section=Section, should_flatten=false), + column='Contents' + )} + ) + WHERE URIs + + /* Parse a deb822 sources file with options in individual columns, flattened: */ + LET Deb822_Flattened(OSPath) = SELECT * FROM flatten(query={ + SELECT OSPath, *, URIComponents(URI=URIs).URIBase AS _URIBase, + URIComponents(URI=URIs).Transport AS _Transport + FROM Deb822_Flattened_(OSPath=OSPath) + }) + + /* Parse an apt sources/list file */ + LET parse_aptsources(OSPath, should_flatten) = if( + condition=OSPath=~'.list$', + then=if(condition=should_flatten, + then=DebOneLine_Flattened(OSPath=OSPath), + else=DebOneLine(OSPath=OSPath) + ), + else=if(condition=should_flatten, + then=Deb822_Flattened(OSPath=OSPath), + else=Deb822(OSPath=OSPath) + ) + ) + + LET files = SELECT OSPath FROM glob( + globs=linuxAptSourcesGlobs.ListGlobs) + + LET deb_sources = SELECT * FROM foreach(row=files, + query={SELECT * FROM parse_aptsources(OSPath=OSPath, should_flatten=true)} + ) + parameters: - name: linuxAptSourcesGlobs - description: Globs to find apt source *.list files. - default: /etc/apt/sources.list,/etc/apt/sources.list.d/*.list - - name: aptCacheDirectory + description: Globs to find apt source *.list and .sources files. + type: csv + default: | + ListGlobs + /etc/apt/sources.list + /etc/apt/sources.list.d/*.list + /etc/apt/sources.list.d/*.sources + - name: aptCacheDirectory description: Location of the apt cache directory. default: /var/lib/apt/lists/ + +precondition: + SELECT OS From info() where OS = 'linux' + sources: - - precondition: - SELECT OS From info() where OS = 'linux' + - name: Sources query: | - /* Search for files which may contain apt sources. The user can - pass new globs here. */ - LET files = SELECT FullPath from glob( - globs=split(string=linuxAptSourcesGlobs, sep=",")) - - /* Read each line in the sources which is not commented. - Deb lines look like: - deb [arch=amd64] http://dl.google.com/linux/chrome-remote-desktop/deb/ stable main - Contains URL, base_uri and components. - */ - LET deb_sources = SELECT * - FROM parse_records_with_regex( - file=files.FullPath, - regex="(?m)^ *(?Pdeb(-src)?) (?:\\[arch=(?P[^\\]]+)\\] )?" + - "(?Phttps?://(?P[^ ]+))" + - " +(?P.+)") - - /* We try to get at the Release file in /var/lib/apt/ by munging + /* Output sources in a readable format: */ + SELECT * FROM foreach(row=files, + query={SELECT * FROM parse_aptsources(OSPath=OSPath, should_flatten=false)} + ) + notebook: + - type: vql_suggestion + name: Only enabled sources + template: | + /* + # Sources (enabled only) + */ + SELECT * FROM source() + WHERE Enabled =~ '(?i)^(?:yes|true|with|on|enable)$' || true + + - type: vql_suggestion + name: Trusted sources (apt-secure bypassed) + template: | + /* + # "Trusted" sources (apt-secure bypassed) + + When the Trusted option is true, apt does not verify the GPG + signature of the Release files of the repository, and it also + doe not warn about this. + */ + SELECT * FROM source() + WHERE Trusted =~ '(?i)^(?:yes|true|with|on|enable)$' || false + + - type: vql_suggestion + name: Hide embedded GPG keys + template: | + /* + # Sources (embedded GPG keys hidden) + */ + SELECT *, if(condition=get(field='Signed-By')=~'BEGIN PGP PUBLIC KEY', + then='(embedded)', else=get(field='Signed-By')) AS `Signed-By` + FROM source() + + - name: SourcesFlattened + query: | + /* Output sources flattened for ease of analysis: */ + SELECT * FROM deb_sources + + - name: SourcesCacheFiles + query: | + /* We try to get at the Release file in /var/lib/apt/ by munging the components and URL. Strip the last component off, convert / and space to _ and - add _Release to get the filename. - */ - LET parsed_apt_lines = SELECT Arch, URL, - base_uri + " " + components as Name, Type, - FullPath as Source, aptCacheDirectory + regex_replace( + add _Release/_InRelease to get the filename. + */ + LET parsed_apt_lines = SELECT get(field='Architectures', default='') AS Architectures, URIs, + _URIBase + " " + Suites + " " + Components as Name, Types, + OSPath as Source, aptCacheDirectory + regex_replace( replace="_", re="_+", source=regex_replace( replace="_", re="[ /]", - source=base_uri + "_dists_" + regex_replace( - source=components, - replace="", re=" +[^ ]+$")) + "_Release" - ) as cache_file - FROM deb_sources - - /* This runs if the file was found. Read the entire file into - memory and parse the same record using multiple RegExps. - */ - LET parsed_cache_files = SELECT Name, Arch, URL, Type, - Source, parse_string_with_regex( - string=Record, - regex=["Codename: (?P[^\\s]+)", - "Version: (?P[^\\s]+)", - "Origin: (?P[^\\s]+)", - "Architectures: (?P[^\\s]+)", - "Components: (?P[^\\s]+)"]) as Record - FROM parse_records_with_regex(file=cache_file, regex="(?sm)(?P.+)") + source=_URIBase + "_dists_" + Suites + )) as cache_file + FROM deb_sources + GROUP BY URIs, Suites + + /* This runs if the file was found. Reads the entire file into + memory and parses the same record using multiple regular expressions. + */ + LET parsed_cache_files(file) = SELECT Name, Architectures, URIs, Types, + Source, parse_string_with_regex( + string=regex_replace(source=Record, + re='(?m)^Version: GnuPG v.+$', replace='' + ), + regex=["Codename: (?P[^\\n]+)", + "Version: (?P[^\\n]+)", + "Origin: (?P[^\\n]+)", + "Architectures: (?P[^\\n]+)", + "Components: (?P[^\\n]+)"]) as Record + FROM parse_records_with_regex(file=file, regex="(?sm)(?P.+)") // Foreach row in the parsed cache file, collect the FileInfo too. - LET add_stat_to_parsed_cache_file = SELECT * from foreach( + LET add_stat_to_parsed_cache_file(file) = SELECT * from foreach( query={ - SELECT FullPath, Mtime, Ctime, Atime, Record, Type, - Name, Arch, URL, Source from stat(filename=cache_file) - }, row=parsed_cache_files) + SELECT OSPath, Mtime, Ctime, Atime, Record, Types, + Name, Architectures, URIs, Source from stat(filename=file) + }, row=parsed_cache_files(file=file)) + WHERE Record + GROUP BY OSPath /* For each row in the parsed file, run the appropriate query depending on if the cache file exists. @@ -90,16 +477,26 @@ sources: */ LET parse_cache_or_pass = SELECT * from if( condition={ - SELECT * from stat(filename=cache_file) + SELECT * from stat(filename=cache_file + '_InRelease') }, - then=add_stat_to_parsed_cache_file, - else={ - SELECT Source, Null as Mtime, Null as Ctime, - Null as Atime, Type, - Null as Record, Arch, URL, Name from scope() + then=add_stat_to_parsed_cache_file(file=cache_file + '_InRelease'), + else={SELECT * FROM if( + condition={ + SELECT * from stat(filename=cache_file + '_Release') + }, + then=add_stat_to_parsed_cache_file(file=cache_file + '_Release'), + else={ + SELECT Source, NULL AS OSPath, Null as Mtime, Null as Ctime, + Null as Atime, Types, + Null as Record, Architectures, URIs, Name from scope() + }) }) -- For each parsed apt .list file line produce some output. SELECT * from foreach( - row=parsed_apt_lines, - query=parse_cache_or_pass) + row={ + SELECT * FROM parsed_apt_lines + }, + query={ + SELECT * FROM parse_cache_or_pass + }) diff --git a/artifacts/definitions/Linux/Debian/Packages.yaml b/artifacts/definitions/Linux/Debian/Packages.yaml index 290db5210..ea484a2be 100644 --- a/artifacts/definitions/Linux/Debian/Packages.yaml +++ b/artifacts/definitions/Linux/Debian/Packages.yaml @@ -1,30 +1,198 @@ name: Linux.Debian.Packages -description: Parse dpkg status file. +author: Andreas Misje – @Misje +description: | + List all packages installed on the system, both deb packages and "snaps". + + The installed deb package information is fetched from the DPKG status file, + while the snap package list is fetched from the snap daemon through a UNIX + socket HTTP call (since detailed snap package information is not easily + found in files). + + The following columns are parsed from the DPKG status file: + + - Package + - _SelectionState (install, hold, deinstall, purge, unknown) + - _Flag (ok, reinstreq) + - State (not-installed, config-files, half-installed, unpacked, + half-configured, triggers-awaited, triggers-pending, installed) + - InstalledSize + - Version + - Source + - _Description + - Architecture + + The following columns are parsed from the snap package response (/v2/snaps): + + - Name + - _Summary + - _Description + - Status (available, installed, active, removed, priced) + - InstalledSize + - Publisher + - InstalledAt + - Version + - Channel + + Both package sources provide more information than this and, and the artifact + can easily be modified to include more details. + parameters: - name: linuxDpkgStatus + description: The DPKG status file to read deb package information from default: /var/lib/dpkg/status + + - name: snapdSocket + description: | + The location of the snap deamon UNIX socket, used for fetching the snap + list through a HTTP API call. If snap is not used, the failed query + response will simply be ignored. + default: /run/snapd.socket + +implied_permissions: + - NETWORK + +precondition: | + SELECT OS + FROM info() + WHERE OS = 'linux' + sources: - - precondition: | - SELECT OS From info() where OS = 'linux' + - name: DebPackages + notebook: + - type: none + query: | - /* First pass - split file into records start with - Package and end with \n\n. + LET ColumnTypes <= dict(`_Description`='nobreak') - Then parse each record using multiple RegExs. - */ - LET packages = SELECT parse_string_with_regex( + + /* First pass - split file into records starting with + Package and ending with \n\n. + Then parse each record using multiple regular expressions. + */ + LET packages = SELECT + parse_string_with_regex( string=Record, - regex=['Package:\\s(?P.+)', - 'Installed-Size:\\s(?P.+)', - 'Version:\\s(?P.+)', - 'Source:\\s(?P.+)', - 'Architecture:\\s(?P.+)']) as Record - FROM parse_records_with_regex( - file=linuxDpkgStatus, - regex='(?sm)^(?PPackage:.+?)\\n\\n') - - SELECT Record.Package as Package, - atoi(string=Record.InstalledSize) as InstalledSize, - Record.Version as Version, - Record.Source as Source, - Record.Architecture as Architecture from packages + regex=['''Package:\s(?P.+)''', + '''Status:\s(?P\S+)\s(?P\S+)\s(?P\S+)''', + '''Installed-Size:\s(?P.+)''', + '''Version:\s(?P.+)''', + '''Source:\s(?P.+)''', + '''Description:\s+(?P.+(\n\s+.+)*)''', + '''Architecture:\s(?P.+)''']) AS Record + FROM parse_records_with_regex( + file=linuxDpkgStatus, + regex='''(?sm)^(?PPackage:.+?)\n\n''') + + SELECT + Record.Package AS Package, + Record.SelectionState AS _SelectionState, + Record.Flag AS _Flag, + Record.State AS State, + humanize(bytes=atoi(string=Record.InstalledSize)) AS InstalledSize, + Record.Version AS Version, + Record.Source AS Source, + regex_replace(source=Record.Description, re='''^\s+\.$''') AS _Description, + Record.Architecture AS Architecture + FROM packages + + - name: Snaps + query: | + LET ColumnTypes <= dict(`_Summary`='nobreak', `_Description`='nobreak') + + LET SnapSocketCheck = SELECT parse_json(data=Content).result AS Result + FROM http_client(url=snapdSocket + ':unix/v2/snaps') + WHERE Response = 200 OR NOT log(message="Error fetching snap: %v", + args=Content) + + // linter: symbol_mask_warn:version + SELECT * + FROM foreach(row=SnapSocketCheck, + query={ + SELECT name AS Name, + summary AS _Summary, + description AS _Description, + status AS Status, + humanize(bytes=`installed-size`) AS InstalledSize, + publisher.`display-name` AS Publisher, + timestamp(string=`install-date`) AS InstalledAt, + version AS Version, + channel AS Channel, + id AS PackageId + FROM foreach(row=Result) + }) + + notebook: + - type: vql + template: | + LET ColumnTypes <= dict(`_Description`='nobreak') + + /* + # All traces of installed deb packages + + Packages that has either been + + - Removed/purged, but it has once been installed ("not-installed") + - Removed, but with configuration files left behind ("config-files") + - Partially installed ("half-installed", "unpacked", "half-configured", + "triggers-awaited", "triggers-pending")) + - Installed ("installed") + + A package that has never been installed will not have made an entry in this + database. + */ + SELECT + * + FROM source( + source="DebPackages") + + /* + # All traces of installed snaps + + Snaps on the system should either be + + - installed + - active + - removed + + Two additional statuses are possible: + + - available + - priced + */ + SELECT + * + FROM source( + source="Snaps") + + /* + # All installed packages (deb and snap combined) + + Only fully installed packages are included. + */ + SELECT + * + FROM chain( + debs={ + SELECT + Package AS Name, + 'deb' AS Type, + InstalledSize, + Version, + _Description, + Architecture + FROM source( + source="DebPackages") + WHERE NOT State IN ("not-installed", "config-files") + }, + snaps={ + SELECT + Name, + 'snap' AS Type, + InstalledSize, + Version, + _Description, + NULL AS Architecture + FROM source( + source="Snaps") + WHERE Status IN ("installed", "active") + }) diff --git a/artifacts/definitions/Linux/Detection/AnomalousFiles.yaml b/artifacts/definitions/Linux/Detection/AnomalousFiles.yaml new file mode 100644 index 000000000..d5e58ec4d --- /dev/null +++ b/artifacts/definitions/Linux/Detection/AnomalousFiles.yaml @@ -0,0 +1,43 @@ +name: Linux.Detection.AnomalousFiles + +description: | + Detects anomalous files in a Linux filesystem. + + An anomalous file is considered one that matches at least one criteria: + + - Hidden (prefixed with a dot); + + - Large, with a size over a specified limit; or + + - With SUID bit set. + +author: George-Andrei Iosif (@iosifache) + +type: CLIENT + +parameters: + - name: MaxNormalSize + description: Size (in bytes) above which a file is considered large + type: int + default: 10485760 + - name: PathsToSearch + description: Paths to search, separated by comma + type: str + default: "/home/**,tmp/**" + +sources: + - precondition: | + SELECT OS + FROM info() + WHERE OS = 'linux' + + query: | + SELECT Fqdn AS Host, + OSPath, + substr(str=Name, start=0, end=1) = "." AS IsHidden, + Size, + Size > MaxNormalSize AS IsLarge, + Mode.String AS Mode, + Mode =~ "^u" as HasSUID + FROM glob(globs=split(string=PathsToSearch, sep_string=",")) + WHERE IsHidden OR IsLarge OR HasSUID diff --git a/artifacts/definitions/Linux/Detection/ImmutableFiles.yaml b/artifacts/definitions/Linux/Detection/ImmutableFiles.yaml new file mode 100644 index 000000000..51a95b972 --- /dev/null +++ b/artifacts/definitions/Linux/Detection/ImmutableFiles.yaml @@ -0,0 +1,17 @@ +name: Linux.Detection.ImmutableFiles +description: | + Returns all immutable files (that have i file attribute set) in a given path + +# Can be CLIENT, CLIENT_EVENT, SERVER, SERVER_EVENT +type: CLIENT + +parameters: + - name: Path + description: Path to scan for immutable files + +sources: + - precondition: + SELECT OS From info() where OS = 'linux' + + query: | + SELECT * from glob(globs=Path) WHERE lsattr(file=FullPath).String =~ "(?-i)i" diff --git a/artifacts/definitions/Linux/Detection/Yara/Process.yaml b/artifacts/definitions/Linux/Detection/Yara/Process.yaml index 898e4cfd2..b95ef29ac 100644 --- a/artifacts/definitions/Linux/Detection/Yara/Process.yaml +++ b/artifacts/definitions/Linux/Detection/Yara/Process.yaml @@ -1,22 +1,25 @@ name: Linux.Detection.Yara.Process author: Matt Green - @mgreen27 description: | - This artifact enables running Yara over processes in memory. + This artifact enables running YARA over processes in memory. - There are 2 kinds of Yara rules that can be deployed: + There are 2 kinds of YARA rules that can be deployed: - 1. Url link to a yara rule. - 2. A Standard Yara rule attached as a parameter. + 1. Url link to a YARA rule. + 2. A Standard YARA rule attached as a parameter. - Only one method of Yara will be applied and search order is as above. The + Only one method of YARA will be applied and search order is as above. The default is Cobalt Strike opcodes. Regex parameters can be applied for process name and pid for targeting. The - artifact also has an option to upload any process with Yara hits. + artifact also has an option to upload any process with YARA hits. - Note: the Yara scan will stop after one hit. Multi-string rules will also only + Note: the YARA scan will stop after one hit. Multi-string rules will also only show one string in returned rows. +aliases: +- MacOS.Detection.Yara.Process + type: CLIENT parameters: - name: ProcessRegex @@ -29,6 +32,7 @@ parameters: type: bool - name: YaraUrl description: If configured will attempt to download Yara rules from Url + type: upload - name: YaraRule type: yara description: Final Yara option and the default if no other options provided. @@ -40,42 +44,41 @@ parameters: condition: any of them } - - name: PathWhitelist - description: | - Process paths to exclude. Default is common AntiVirus we have seen cause - false positives with signatures in memory. - type: csv - default: | - Path - /usr/local/bin/velociraptor + - name: NumberOfHits + description: THis artifact will stop by default at one hit. This setting allows additional hits + default: 1 + type: int + - name: ContextBytes + description: Include this amount of bytes around hit as context. + default: 0 + type: int + - name: ExePathWhitelist + description: Regex of ProcessPaths to exclude + type: regex sources: - precondition: - SELECT OS From info() where OS = 'linux' + SELECT OS From info() where OS = 'linux' OR OS = 'darwin' query: | -- check which Yara to use - LET yara <= if(condition=YaraUrl, - then= { SELECT Content FROM http_client( url=YaraUrl, method='GET') }, - else= { SELECT YaraRule as Content FROM scope() }) + LET yara_rules <= YaraUrl || YaraRule -- find velociraptor process LET me = SELECT Pid FROM pslist(pid=getpid()) - -- allow whitelist case sensitive - LET whitelist <= SELECT upcase(string=Path) AS Path FROM PathWhitelist - -- find all processes and add filters LET processes = SELECT Name as ProcessName, - Cmdline AS CommandLine, Pid + CommandLine, Pid FROM pslist() WHERE Name =~ ProcessRegex AND format(format="%d", args=Pid) =~ PidRegex AND NOT Pid in me.Pid - AND NOT upcase(string=Exe) in whitelist.Path + AND NOT if(condition=ExePathWhitelist, + then= Exe=~ExePathWhitelist) AND log(message=format(format="Scanning pid %v: %v", args=[ Pid, CommandLine])) @@ -87,15 +90,24 @@ sources: ProcessName, CommandLine, Pid, - Namespace, Rule, + Tag, Meta, + String.Name as YaraString, String.Offset as HitOffset, - String.Name as HitName, - String.HexData as HitHexData - FROM yara(files=format(format="/%d", args=Pid), - accessor='process',rules=yara.Content[0]) - LIMIT 1 + if(condition=String.Data, + then=upload( + accessor='scope', + file='String.Data', + name=format(format="%v-%v_%v_%v", + args=[ ProcessName, Pid, String.Offset, ContextBytes ] + ))) as HitContext + FROM proc_yara( + pid=Pid, + rules=yara_rules, + context=ContextBytes, + number=NumberOfHits + ) }) -- upload hits using the process accessor @@ -103,8 +115,8 @@ sources: upload( accessor="process", file=format(format="/%v", args=Pid), - name=format(format='%v-%v.dmp', - args= [ ProcessName, Pid ])) as ProcessDump + name=pathspec(Path=format(format='%v-%v.dmp', + args= [ ProcessName, Pid ]))) as ProcessDump FROM hits WHERE log(message=format(format='Will upload %v: %v', args=[Pid, ProcessName])) @@ -112,3 +124,7 @@ sources: SELECT * FROM if(condition=UploadHits, then=upload_hits, else=hits) + +column_types: + - name: HitContext + type: preview_upload diff --git a/artifacts/definitions/Linux/Events/EBPF.yaml b/artifacts/definitions/Linux/Events/EBPF.yaml new file mode 100644 index 000000000..6bc47dba4 --- /dev/null +++ b/artifacts/definitions/Linux/Events/EBPF.yaml @@ -0,0 +1,45 @@ +name: Linux.Events.EBPF +description: | + This artifact forwards EBPF events generated on the endpoint. + +precondition: | + SELECT OS From info() where OS = 'linux' + +type: CLIENT_EVENT + +parameters: + - name: Events + description: Events to forward + type: csv + default: | + Event,Desc,Enabled + bpf_attach,A bpf program is attached,Y + chdir,Process changes directory,N + fchownat,File ownership is changed,Y + file_modification,A process changes the ctime of a file,N + kill,Kill another process,Y + magic_write,Intercepts file writes to capture the header magic,N + mkdir,Process makes new directory,N + module_free,A module is unloaded from the kernel,Y + mount,A filesystem is mounted,Y + openat,A process is opening a file (noisy),N + openat2,A process is opening a file (noisy),N + sched_process_exec,A process starts,Y + sched_process_exit,A process ends,Y + security_file_open,Files are opened,Y + security_inode_mknod,A new node is created with mknod (e.g. fifo or device file),Y + security_inode_rename,File is being renamed,N + security_inode_symlink,Create a symlink,Y + security_kernel_post_read_file,Fires when the kernel reads a file (e.g. module),Y + security_socket_accept,A process accepted a connection,Y + security_socket_bind,A process bind to a local port,Y + security_socket_connect,A process is making a connection,Y + setxattr,Setting and extended attribute to a file,Y + umount2,A filesystem is being unmounted,Y + unlink,A file is deleted,Y + +sources: + - query: | + LET SelectedEvents <= SELECT * FROM Events WHERE Enabled =~ "Y" + + SELECT * FROM watch_ebpf(events=SelectedEvents.Event) diff --git a/artifacts/definitions/Linux/Events/HTTPConnections.yaml b/artifacts/definitions/Linux/Events/HTTPConnections.yaml new file mode 100644 index 000000000..421b6c299 --- /dev/null +++ b/artifacts/definitions/Linux/Events/HTTPConnections.yaml @@ -0,0 +1,57 @@ +name: Linux.Events.HTTPConnections +description: | + This artifact uses eBPF to track HTTP and parse connections from + various processes. + + NOTE: This event is generated from network traffic - it is unable to + view TLS encrypted data. + + If the process tracker is enabled we also show more information + about the process. + +type: CLIENT_EVENT + +precondition: | + SELECT OS From info() where OS = 'linux' + +parameters: + - name: HostFilter + description: Filter Events by Host header + type: regex + default: . + - name: URLFilter + description: Filter Events by URL + type: regex + default: . + - name: ProcessNameFilter + description: Filter Events by Process Name + type: regex + default: . + - name: IncludeHeaders + type: bool + description: If set we include more details like HTTP Headers + - name: IncludeProcessInfo + type: bool + description: If set we include more process information. + +sources: + - query: | + // linter: symbol_mask_warn:host + + SELECT System.Timestamp AS Timestamp, + System.ProcessName AS ProcessName, + System.ProcessID AS Pid, + if(condition=IncludeProcessInfo, + then=process_tracker_get(id=System.ProcessID).Data) AS ProcessInfo, + EventData.metadata.src_ip AS src_ip, + EventData.metadata.src_port AS src_port, + EventData.metadata.dst_ip AS dest_ip, + EventData.metadata.dst_port AS dest_port, + EventData.http_request.host AS host, + EventData.http_request.uri_path AS uri_path, + if(condition=IncludeHeaders, + then=EventData.http_request) AS _HTTPRequest + FROM watch_ebpf(events="net_packet_http_request") + WHERE host =~ HostFilter + AND uri_path =~ URLFilter + AND ProcessName =~ ProcessNameFilter diff --git a/artifacts/definitions/Linux/Events/Journal.yaml b/artifacts/definitions/Linux/Events/Journal.yaml new file mode 100644 index 000000000..19bcdafdc --- /dev/null +++ b/artifacts/definitions/Linux/Events/Journal.yaml @@ -0,0 +1,21 @@ +name: Linux.Events.Journal +description: | + Watches the binary journal logs. Systemd uses a binary log format to + store logs. + +type: CLIENT_EVENT + +parameters: +- name: JournalGlob + type: glob + description: A Glob expression for finding journal files. + default: /{run,var}/log/journal/*/*.journal + +sources: +- query: | + SELECT * FROM foreach(row={ + SELECT OSPath FROM glob(globs=JournalGlob) + }, query={ + SELECT * + FROM watch_journald(filename=OSPath) + }, workers=100) diff --git a/artifacts/definitions/Linux/Events/ProcessExecutions.yaml b/artifacts/definitions/Linux/Events/ProcessExecutions.yaml index 5d055512c..b9308564d 100644 --- a/artifacts/definitions/Linux/Events/ProcessExecutions.yaml +++ b/artifacts/definitions/Linux/Events/ProcessExecutions.yaml @@ -1,31 +1,24 @@ name: Linux.Events.ProcessExecutions -description: | - This artifact collects process execution logs from the Linux kernel. - - This artifact relies on the presence of `auditctl` usually included - in the auditd package. On Ubuntu you can install it using: - ``` - apt-get install auditd - ``` - -precondition: SELECT OS From info() where OS = 'linux' +description: | + This artifact collects process execution events using the execsnoop eBPF plugin + if the client binary supports it and the kernel is 5.8+. Otherwise, the audit + plugin is used. type: CLIENT_EVENT -required_permissions: - - EXECVE +sources: + - precondition: | + SELECT OS, KernelVersion, + parse_string_with_regex(string=KernelVersion, regex='^(?P[0-9]+.[0-9]+)') AS parsed + FROM info() + WHERE OS = 'linux' + AND (version(plugin='execsnoop') = Null OR parse_float(string=parsed.kernel_ver) < 5.8) -parameters: - - name: pathToAuditctl - default: /sbin/auditctl - description: We depend on auditctl to install the correct process execution rules. + query: | + LET MachineID <= strip(string=read_file(filename="/etc/machine-id"), suffix="\n") -sources: - - query: | - // Install the auditd rule if possible. - LET _ <= SELECT * FROM execve(argv=[pathToAuditctl, "-a", - "exit,always", "-F", "arch=b64", "-S", "execve", "-k", "procmon"]) + LET proc_exec_rules = ("-a always,exit -F arch=b64 -S execve -k vrr_procmon", "-a always,exit -F arch=b32 -S execve -k vrr_procmon") LET exec_log = SELECT timestamp(string=Timestamp) AS Time, Sequence, atoi(string=Process.PID) AS Pid, @@ -35,18 +28,50 @@ sources: Process.Title AS CmdLine, Process.Exe AS Exe, Process.CWD AS CWD - FROM audit() - WHERE "procmon" in Tags AND Result = 'success' + FROM audit(rules=proc_exec_rules) + WHERE "vrr_procmon" in Tags AND Result = 'success' + + LET hash_log = SELECT *, + hash(path=Exe, hashselect=['SHA256']) AS hashes + FROM exec_log // Cache Uid -> Username mapping. LET users <= SELECT User, atoi(string=Uid) AS Uid FROM Artifact.Linux.Sys.Users() // Enrich the original artifact with more data. - SELECT Time, Pid, Ppid, UserId, + SELECT Time, MachineID, Pid, Ppid, UserId, { SELECT User from users WHERE Uid = UserId} AS User, - regex_replace(source=read_file(filename= "/proc/" + PPID + "/cmdline"), - replace=" ", re="[\\0]") AS Parent, CmdLine, - Exe, CWD - FROM exec_log + Exe, CWD, + hashes.SHA256 AS SHA256 + FROM hash_log + + - precondition: | + SELECT OS, KernelVersion, + parse_string_with_regex(string=KernelVersion, regex='^(?P[0-9]+.[0-9]+)') AS parsed + FROM info() + WHERE OS = 'linux' + AND version(plugin='execsnoop') != Null + AND parse_float(string=parsed.kernel_ver) >= 5.8 + + query: | + LET MachineID <= strip(string=read_file(filename="/etc/machine-id"), suffix="\n") + + LET exec_log = SELECT * FROM execsnoop() + + LET hash_log = SELECT *, + hash(path=Exe, hashselect=['SHA256']) AS hashes + FROM exec_log + + // Cache Uid -> Username mapping. + LET users <= SELECT User, atoi(string=Uid) AS UserID + FROM Artifact.Linux.Sys.Users() + + SELECT Time, MachineID, Pid, Ppid, Uid, + { SELECT User from users WHERE UserID = Uid } AS User, + Argv AS CmdLine, + Exe, + Cwd AS CWD, + hashes.SHA256 AS SHA256 + FROM hash_log diff --git a/artifacts/definitions/Linux/Events/SSHBruteforce.yaml b/artifacts/definitions/Linux/Events/SSHBruteforce.yaml index ab638ae05..8dd4945c0 100644 --- a/artifacts/definitions/Linux/Events/SSHBruteforce.yaml +++ b/artifacts/definitions/Linux/Events/SSHBruteforce.yaml @@ -1,13 +1,13 @@ name: Linux.Events.SSHBruteforce description: | - This is a monitoring artifact which detects a successful SSH login - preceded by some failed attempts within the last hour. - - This is particularly important in the case of ssh brute forcers. If - one of the brute force password attempts succeeded the password - guessing program will likely report the success and move on. This - alert might provide sufficient time for admins to lock down the - account before attackers can exploit the weak password. + A monitoring artifact which detects a successful SSH login preceded by some + failed attempts within the last hour. + + This is particularly important in the case of SSH brute force attacks. If one + of the brute force password attempts succeeded, the password guessing program + will likely report the success and move on. This alert might provide + sufficient time for admins to lock down the account before attackers can + exploit the weak password. reference: - https://www.elastic.co/blog/grokking-the-linux-authorization-logs diff --git a/artifacts/definitions/Linux/Events/SSHLogin.yaml b/artifacts/definitions/Linux/Events/SSHLogin.yaml index 4eeb67222..9808b5929 100644 --- a/artifacts/definitions/Linux/Events/SSHLogin.yaml +++ b/artifacts/definitions/Linux/Events/SSHLogin.yaml @@ -1,6 +1,6 @@ name: Linux.Events.SSHLogin description: | - This monitoring artifact watches the auth.log file for new + This monitoring artifact watches the system logs for new successful SSH login events and relays them back to the server. reference: @@ -12,13 +12,19 @@ parameters: - name: syslogAuthLogPath default: /var/log/auth.log + - name: SSHSystemdUnit + description: Systemd Unit responsible for sshd + default: sshd.service + - name: SSHGrok description: A Grok expression for parsing SSH auth lines. default: >- - %{SYSLOGTIMESTAMP:timestamp} (?:%{SYSLOGFACILITY} )?%{SYSLOGHOST:logsource} %{SYSLOGPROG}: %{DATA:event} %{DATA:method} for (invalid user )?%{DATA:user} from %{IPORHOST:ip} port %{NUMBER:port} ssh2(: %{GREEDYDATA:system.auth.ssh.signature})? + (%{SYSLOGTIMESTAMP:timestamp} (?:%{SYSLOGFACILITY} )?%{SYSLOGHOST:logsource} %{SYSLOGPROG}: )?%{DATA:event} %{DATA:method} for (invalid user )?%{DATA:user} from %{IPORHOST:ip} port %{NUMBER:port} ssh2(: %{GREEDYDATA:system.auth.ssh.signature})? sources: - - query: | + - precondition: SELECT OS From info() where OS = 'linux' + description: Collect successful SSH login attempts from syslog + query: | -- Basic syslog parsing via GROK expressions. LET success_login = SELECT grok(grok=SSHGrok, data=Line) AS Event, Line FROM watch_syslog(filename=syslogAuthLogPath) @@ -30,3 +36,40 @@ sources: Event.IP AS SourceIP, Event.pid AS Pid FROM success_login + - precondition: SELECT OS From info() where OS = 'linux' + description: Collect successful SSH login attempts from systemd journal + query: | + LET success_login = SELECT REALTIME_TIMESTAMP, _PID, grok(grok=SSHGrok, data=MESSAGE) AS Event + FROM watch_journal() + WHERE _TRANSPORT != 'kernel' AND _SYSTEMD_UNIT = SSHSystemdUnit AND Event.event = "Accepted" + SELECT timestamp(epoch=REALTIME_TIMESTAMP) AS Time, + Event.user AS User, + Event.method AS Method, + Event.ip as SourceIP, + _PID AS Pid + FROM success_login + +reports: + - type: MONITORING_DAILY + template: | + + {{ define "journald" }} + SELECT * + FROM source() + ORDER BY Time DESC + {{end}} + + SSH Logins + ============ + + {{ .Description }} + + The following tables shows basic information about login events via SSH on this system. + + {{ Query "journald" | Table }} + + The following VQL queries were used to create the table above. + + ```sql + {{ template "journald" }} + ``` diff --git a/artifacts/definitions/Linux/Events/TrackProcesses.yaml b/artifacts/definitions/Linux/Events/TrackProcesses.yaml new file mode 100644 index 000000000..765be94f5 --- /dev/null +++ b/artifacts/definitions/Linux/Events/TrackProcesses.yaml @@ -0,0 +1,78 @@ +name: Linux.Events.TrackProcesses +description: | + This artifact uses eBPF and pslist to keep track of running + processes by using the Velociraptor process tracker. + + The process tracker keeps track of exited processes, and resolves + process call chains from it in memory cache. + + This event artifact enables the global process tracker and makes it + possible to run many other artifacts that depend on the process + tracker. + + NOTE: Unlike `Windows.Events.TrackProcesses`, the eBPF program is + already built into Velociraptor so this artifact does not depend on + external tools. + +precondition: | + SELECT OS From info() where OS = 'linux' + +type: CLIENT_EVENT + +parameters: + - name: AlsoForwardUpdates + type: bool + description: Upload all tracker state updates to the server + - name: MaxSize + type: int64 + description: Maximum size of the in-memory process cache (default 10k) + +sources: + - query: | + LET SyncQuery = SELECT + Pid AS id, + Ppid AS parent_id, + CreateTime AS start_time, + dict(Name=Name, + Username=Username, + Exe=Exe, + CreateTime=CreateTime, + CommandLine=CommandLine) AS data + FROM pslist() + + LET UpdateQuery = SELECT * FROM foreach( + row={ + SELECT * FROM watch_ebpf(events=["sched_process_exit", "sched_process_exec"]) + }, query={ + SELECT * FROM switch(a={ + SELECT System.HostProcessID AS id, + System.HostParentProcessID AS parent_id, + "start" AS update_type, + dict(Pid=System.HostProcessID, + Ppid=System.HostParentProcessID, + Name=System.ProcessName, + Username=System.UserID, + Exe=EventData.cmdpath, + CommandLine=join(array=EventData.argv, sep=" ")) AS data, + + System.Timestamp AS start_time, + NULL AS end_time + FROM scope() + WHERE System.EventName =~ "exec" + }, end={ + SELECT System.HostProcessID AS id, + NULL AS parent_id, + "exit" AS update_type, + dict() AS data, + NULL AS start_time, + System.Timestamp AS end_time + FROM scope() + WHERE System.EventName =~ "exit" + }) + }) + + LET Tracker <= process_tracker(max_size=MaxSize, + sync_query=SyncQuery, update_query=UpdateQuery, sync_period=60000) + + SELECT * FROM process_tracker_updates() + WHERE update_type = "stats" OR AlsoForwardUpdates diff --git a/artifacts/definitions/Linux/Forensics/ImmutableFiles.yaml b/artifacts/definitions/Linux/Forensics/ImmutableFiles.yaml new file mode 100644 index 000000000..8904b1fa0 --- /dev/null +++ b/artifacts/definitions/Linux/Forensics/ImmutableFiles.yaml @@ -0,0 +1,48 @@ +name: Linux.Forensics.ImmutableFiles +description: | + Searches the filesystem for immutable files. + + Attackers sometimes enable immutable files in Linux. This prevents files from + being modified. However this is sometimes a strong signal. + + NOTE: We use the ext4 accessor to parse the low level filesystem. + +precondition: | + SELECT * FROM info() where OS = 'linux' + +parameters: + - name: SearchFilesGlob + default: /home/* + description: Use a glob to define the files that will be searched. + - name: OneFilesystem + default: N + type: bool + description: When set we do not follow a link to go on to a different filesystem. + + - name: DoNotFollowSymlinks + type: bool + default: N + description: If specified we are allowed to follow symlinks while globbing + +column_types: + - name: ATime + type: timestamp + - name: MTime + type: timestamp + - name: CTime + type: timestamp + + +sources: +- query: | + SELECT OSPath, + Sys.mft as Inode, + Mode.String AS Mode, Size, + Mtime AS MTime, + Atime AS ATime, + Ctime AS CTime, + IsDir, Mode, Data + FROM glob(globs=SearchFilesGlob, + one_filesystem=OneFilesystem, + accessor="ext4", nosymlink=DoNotFollowSymlinks) + WHERE Data.Flags =~ "IMMUTABLE" diff --git a/artifacts/definitions/Linux/Forensics/Journal.yaml b/artifacts/definitions/Linux/Forensics/Journal.yaml new file mode 100644 index 000000000..0ec7a8a88 --- /dev/null +++ b/artifacts/definitions/Linux/Forensics/Journal.yaml @@ -0,0 +1,119 @@ +name: Linux.Forensics.Journal +description: | + Parses the binary journal logs. Systemd uses a binary log format to + store logs. + +parameters: +- name: JournalGlob + type: glob + description: A Glob expression for finding journal files. + default: /{run,var}/log/journal/*/*.journal +- name: IdentifierRegex + type: regex + description: "Regex of event source e.g sshd or kernel" +- name: IocRegex + type: regex + description: "IOC Regex in event data" +- name: DateAfter + type: timestamp + description: "search for events after this date. YYYY-MM-DDTmm:hh:ssZ" +- name: DateBefore + type: timestamp + description: "search for events before this date. YYYY-MM-DDTmm:hh:ssZ" +- name: AlsoUpload + type: bool + description: If set we also upload the raw files. + +sources: +- name: Uploads + query: | + SELECT * + FROM if(condition=AlsoUpload, + then={ + SELECT OSPath, + upload(file=OSPath) AS Upload + FROM glob(globs=JournalGlob) + }) + + +- query: | + LET standard = SELECT * + FROM foreach(row={ + SELECT OSPath + FROM glob(globs=JournalGlob) + }, + query={ + SELECT * + FROM parse_journald(filename=OSPath, + start_time=DateAfter, + end_time=DateBefore) + }) + + LET identifier_only = SELECT * + FROM foreach(row={ + SELECT OSPath + FROM glob(globs=JournalGlob) + }, + query={ + SELECT * + FROM parse_journald(filename=OSPath, + start_time=DateAfter, + end_time=DateBefore) + WHERE EventData.SYSLOG_IDENTIFIER =~ IdentifierRegex + }) + + LET all_regex = SELECT * + FROM foreach(row={ + SELECT OSPath + FROM glob(globs=JournalGlob) + }, + query={ + SELECT * + FROM parse_journald(filename=OSPath, + start_time=DateAfter, + end_time=DateBefore) + WHERE EventData.SYSLOG_IDENTIFIER =~ IdentifierRegex + AND format(format='%s_%s_%s', + args=[EventData, System._CMDLINE, System._EXE]) =~ + IocRegex + }) + + LET ioc_only = SELECT * + FROM foreach(row={ + SELECT OSPath + FROM glob(globs=JournalGlob) + }, + query={ + SELECT * + FROM parse_journald(filename=OSPath, + start_time=DateAfter, + end_time=DateBefore) + WHERE format(format='%s_%s_%s', + args=[EventData, System._CMDLINE, + System._EXE]) =~ IocRegex + }) + + SELECT * + FROM if(condition=IdentifierRegex + AND IocRegex, + then=all_regex, + else=if(condition=IdentifierRegex, + then=identifier_only, + else=if(condition=IocRegex, then=ioc_only, else=standard))) + + notebook: + - type: vql_suggestion + name: Simplified syslog-like view + template: | + /* + # Simplified log view + */ + LET ColumnTypes<=dict(`_ClientId`='client') + + SELECT System.Timestamp AS Timestamp, + ClientId AS _ClientId, + client_info(client_id=ClientId).os_info.hostname AS Hostname, + EventData.SYSLOG_IDENTIFIER AS Unit, + EventData.MESSAGE AS Message + FROM source() + ORDER BY Timestamp diff --git a/artifacts/definitions/Linux/KapeFiles/CollectFromDirectory.yaml b/artifacts/definitions/Linux/KapeFiles/CollectFromDirectory.yaml deleted file mode 100644 index 134f14067..000000000 --- a/artifacts/definitions/Linux/KapeFiles/CollectFromDirectory.yaml +++ /dev/null @@ -1,2085 +0,0 @@ -name: Linux.KapeFiles.CollectFromDirectory -description: | - - Kape is a popular bulk collector tool for triaging a system - quickly. While KAPE itself is not an opensource tool, the logic it - uses to decide which files to collect is encoded in YAML files - hosted on the KapeFiles project - (https://github.com/EricZimmerman/KapeFiles) and released under an - MIT license. - - This artifact is automatically generated from these YAML files, - contributed and maintained by the community. This artifact only - encapsulates the KAPE "Targets" - basically a bunch of glob - expressions used for collecting files on the endpoint. We do not - do any post processing these files - we just collect them. - - We recommend that timeouts and upload limits be used - conservatively with this artifact because we can upload really - vast quantities of data very quickly. - -reference: - - https://www.kroll.com/en/insights/publications/cyber/kroll-artifact-parser-extractor-kape - - https://github.com/EricZimmerman/KapeFiles - -type: client - -parameters: - - name: Device - description: Path from where to start the search. - default: "/mnt/windows_mount" - - - name: _BasicCollection - description: "Basic Collection (by Phill Moore): $Boot, $J, $J, $LogFile, $MFT, $Max, $Max, $T, $T, Amcache, Amcache, Amcache transaction files, Amcache transaction files, Desktop LNK Files, Desktop LNK Files XP, Event logs Win7+, Event logs Win7+, Event logs XP, LNK Files from C:\ProgramData, LNK Files from Microsoft Office Recent, LNK Files from Recent, LNK Files from Recent (XP), Local Service registry hive, Local Service registry hive, Local Service registry transaction files, Local Service registry transaction files, NTUSER.DAT DEFAULT registry hive, NTUSER.DAT DEFAULT registry hive, NTUSER.DAT DEFAULT transaction files, NTUSER.DAT DEFAULT transaction files, NTUSER.DAT registry hive, NTUSER.DAT registry hive XP, NTUSER.DAT registry transaction files, Network Service registry hive, Network Service registry hive, Network Service registry transaction files, Network Service registry transaction files, PowerShell Console Log, Prefetch, Prefetch, RECYCLER - WinXP, RecentFileCache, RecentFileCache, Recycle Bin - Windows Vista+, RegBack registry transaction files, RegBack registry transaction files, Restore point LNK Files XP, SAM registry hive, SAM registry hive, SAM registry hive (RegBack), SAM registry hive (RegBack), SAM registry transaction files, SAM registry transaction files, SECURITY registry hive, SECURITY registry hive, SECURITY registry hive (RegBack), SECURITY registry hive (RegBack), SECURITY registry transaction files, SECURITY registry transaction files, SOFTWARE registry hive, SOFTWARE registry hive, SOFTWARE registry hive, SOFTWARE registry hive, SOFTWARE registry hive (RegBack), SOFTWARE registry hive (RegBack), SOFTWARE registry transaction files, SOFTWARE registry transaction files, SOFTWARE registry transaction files, SOFTWARE registry transaction files, SRUM, SRUM, SYSTEM registry hive, SYSTEM registry hive, SYSTEM registry hive (RegBack), SYSTEM registry hive (RegBack), SYSTEM registry hive (RegBack), SYSTEM registry hive (RegBack), SYSTEM registry transaction files, SYSTEM registry transaction files, Setupapi.log Win7+, Setupapi.log Win7+, Setupapi.log XP, Syscache, Syscache transaction files, System Profile registry hive, System Profile registry hive, System Profile registry transaction files, System Profile registry transaction files, System Restore Points Registry Hives (XP), Thumbcache DB, UsrClass.dat registry hive, UsrClass.dat registry transaction files, WindowsIndexSearch, XML, XML, at .job, at .job, at SchedLgU.txt, at SchedLgU.txt" - type: bool - - name: _SANS_Triage - description: "SANS Triage Collection (by Mark Hallman): $Boot, $J, $J, $LogFile, $MFT, $Max, $Max, $T, $T, AVG AV Logs, AVG AV Logs (XP), AVG AV Report Logs (XP), AVG Report Logs, ActivitiesCache.db, Addons, Addons XP, Amcache, Amcache, Amcache transaction files, Amcache transaction files, Ammyy Program Data, AnyDesk Logs - ProgramData - *.trace, AnyDesk Logs - ProgramData - connection_trace.txt, AnyDesk Logs - System User Account, AnyDesk Logs - User Profile - *.trace, AnyDesk Logs - User Profile - connection_trace.txt, AnyDesk Videos, Application Event Log Win7+, Application Event Log Win7+, Application Event Log XP, Application Event Log XP, Avast AV Index, Avast AV Logs, Avast AV Logs (XP), Avast AV User Logs, Avira Activity Logs, Bitdefender Endpoint Security Logs, Bitdefender Internet Security Logs, Bitdefender SQLite DB Files, Bookmarks, Bookmarks, Box Drive Application Metadata, Box Sync Application Metadata, Chrome Cookies, Chrome Cookies XP, Chrome Current Session, Chrome Current Session XP, Chrome Current Tabs, Chrome Current Tabs XP, Chrome Download Metadata, Chrome Extension Cookies, Chrome Favicons, Chrome Favicons XP, Chrome History, Chrome History XP, Chrome Last Session, Chrome Last Session XP, Chrome Last Tabs, Chrome Last Tabs XP, Chrome Login Data, Chrome Login Data XP, Chrome Media History, Chrome Network Action Predictor, Chrome Network Persistent State, Chrome Preferences, Chrome Preferences XP, Chrome Quota Manager, Chrome Reporting and NEL, Chrome Sessions Folder, Chrome Shortcuts, Chrome Shortcuts XP, Chrome SyncData Database, Chrome Top Sites, Chrome Top Sites XP, Chrome Trust Tokens, Chrome Visited Links, Chrome Visited Links XP, Chrome Web Data, Chrome Web Data XP, Chrome bookmarks, Chrome bookmarks XP, Cisco Jabber Database, ComboFix, Cookies, Cookies, Cookies XP, Cybereason Anti-Ransomware Logs, Cybereason Application Control and NGAV Logs, Cybereason Sensor Communications and Anti-Malware Logs, Desktop LNK Files, Desktop LNK Files XP, Discord Cache Files, Discord Local Storage LevelDB Files, Downloads, Downloads XP, Dropbox Metadata, Dropbox Metadata, Dropbox Metadata, Dropbox Metadata, Dropbox Metadata, ESET NOD32 AV Logs, ESET NOD32 AV Logs (XP), Edge Bookmarks, Edge Collections, Edge Cookies, Edge Current Session, Edge Current Tabs, Edge Favicons, Edge History, Edge Last Session, Edge Last Tabs, Edge Login Data, Edge Media History, Edge Network Action Predictor, Edge Preferences, Edge Sessions Folder, Edge Shortcuts, Edge SyncData Database, Edge Top Sites, Edge Visited Links, Edge Web Data, Edge bookmarks, Edge folder, Emsisoft Scan Logs, Event logs Win7+, Event logs Win7+, Event logs XP, Extensions, F-Secure Logs, F-Secure Scheduled Scan Reports, F-Secure User Logs, Favicons, Favicons XP, Form history, Form history XP, Google Drive Backup and Sync Metadata, Google Drive for Desktop Metadata, HexChat Chat Logs, HitmanPro Alert Logs, HitmanPro Database, HitmanPro Logs, IE 11 Cookies, IE 11 Metadata, IE 9/10 Cookies, IE 9/10 Download History, IE 9/10 History, IceChat Chat Logs, Index.dat History, Index.dat History subdirectory, Index.dat Office, Index.dat Office XP, Index.dat UserData, Index.dat cookies, Kaseya Agent Edge Service Logs, Kaseya Agent Endpoint Service Logs, Kaseya Agent Endpoint Service Logs (XP), Kaseya Agent Service Log, Kaseya Live Connect Logs, Kaseya Live Connect Logs (XP), Kaseya Setup Log, Kaseya Setup Log, Kaseya Setup Log, LNK Files from C:\ProgramData, LNK Files from Microsoft Office Recent, LNK Files from Recent, LNK Files from Recent (XP), Local Internet Explorer folder, Local Service registry hive, Local Service registry hive, Local Service registry transaction files, Local Service registry transaction files, LocalSessionManager Event Logs, LocalSessionManager Event Logs, LogMeIn Application Logs, LogMeIn ProgramData Logs, MalwareBytes Anti-Malware Logs, MalwareBytes Anti-Malware Scan Logs, MalwareBytes Anti-Malware Scan Results Logs, MalwareBytes Anti-Malware Service Logs, Mattermost - Chat Logs, McAfee Desktop Protection Logs, McAfee Desktop Protection Logs XP, McAfee Endpoint Security Logs, McAfee Endpoint Security Logs, McAfee VirusScan Logs, McAfee ePO Logs, Microsoft Teams Cache, Microsoft Teams Config, Microsoft Teams IndexedDB Cache, Microsoft Teams Local Storage Cache, Microsoft Teams Logs (Windows 11), NTUSER.DAT DEFAULT registry hive, NTUSER.DAT DEFAULT registry hive, NTUSER.DAT DEFAULT transaction files, NTUSER.DAT DEFAULT transaction files, NTUSER.DAT registry hive, NTUSER.DAT registry hive XP, NTUSER.DAT registry transaction files, Network Service registry hive, Network Service registry hive, Network Service registry transaction files, Network Service registry transaction files, OneDrive Metadata Logs, OneDrive Metadata Settings, Opera - Local Folder, Opera - Roaming Folder, Password, Password, Password, Password XP, Password XP, Password XP, Permissions, Places, Places XP, Preferences, Prefetch, Prefetch, Protections, Puffin - Autocomplete Data, Puffin - Cookies, Puffin - Image Cache, Puffin - Password (Encrypted), Puffin - Password Forms Data, Puffin - Subscription Data, Puffin - data.db, RDP Cache Files, RDP Cache Files, RDPClient Event Logs, RDPClient Event Logs, RDPCoreTS Event Logs, RDPCoreTS Event Logs, RECYCLER - WinXP, Radmin Server 32bit Chats, Radmin Server 32bit Log, Radmin Server 64bit Chats, Radmin Server 64bit Log, Radmin Viewer Chats, RealVNC Log, RecentFileCache, RecentFileCache, Recycle Bin - Windows Vista+, RegBack registry transaction files, RegBack registry transaction files, RemoteConnectionManager Event Logs, RemoteConnectionManager Event Logs, Restore point LNK Files XP, Roaming Internet Explorer folder, RogueKiller Reports, SAM registry hive, SAM registry hive, SAM registry hive (RegBack), SAM registry hive (RegBack), SAM registry transaction files, SAM registry transaction files, SECURITY registry hive, SECURITY registry hive, SECURITY registry hive (RegBack), SECURITY registry hive (RegBack), SECURITY registry transaction files, SECURITY registry transaction files, SOFTWARE registry hive, SOFTWARE registry hive, SOFTWARE registry hive, SOFTWARE registry hive, SOFTWARE registry hive (RegBack), SOFTWARE registry hive (RegBack), SOFTWARE registry transaction files, SOFTWARE registry transaction files, SOFTWARE registry transaction files, SOFTWARE registry transaction files, SRUM, SRUM, SUM Database (.mdb files), SUPERAntiSpyware Logs, SYSTEM registry hive, SYSTEM registry hive, SYSTEM registry hive (RegBack), SYSTEM registry hive (RegBack), SYSTEM registry hive (RegBack), SYSTEM registry hive (RegBack), SYSTEM registry transaction files, SYSTEM registry transaction files, ScreenConnect Session Database, ScreenConnect Session Database, ScreenConnect User Config, Search, Search XP, SecureAge Antvirus Logs, SentinelOne EDR Log, Sessionstore, Sessionstore Folder, Sessionstore XP, Signal Attachments cache, Signal Database, Signal Logs, Signal config.json, Signons, Signons XP, Skype for Destkop v8+ Chromium Cache, Slack - Chat Logs, Slack Cache, Slack Electron Logs, Slack LevelDB Files, Slack Storage, Sophos Logs, Sophos Logs (XP), Storage Sync, Supremo Connection Logs, Supremo File Transfer Inbox, Symantec Endpoint Protection Logs, Symantec Endpoint Protection Logs (XP), Symantec Endpoint Protection Quarantine, Symantec Endpoint Protection Quarantine (XP), Symantec Endpoint Protection User Logs, Symantec Event Log Win7+, Symantec Event Log Win7+, Syscache, Syscache transaction files, System Profile registry hive, System Profile registry hive, System Profile registry transaction files, System Profile registry transaction files, System Restore Points Registry Hives (XP), TeamViewer Application Logs, TeamViewer Configuration Files, TeamViewer Connection Logs, Telegram app folder, Telegram downloaded files, Thumbcache DB, TotalAV Logs, TotalAV Logs, Trend Micro Logs, Trend Micro Security Agent Connection Logs, Trend Micro Security Agent Report Logs, UsrClass.dat registry hive, UsrClass.dat registry transaction files, VIPRE Business Agent Logs, VIPRE Business User Logs (up to v4), VIPRE Business User Logs (v5-v6), VIPRE Business User Logs (v7+), Viber Config Database, Viber Users Avatars Cache, Viber Users Backgrounds Cache, Viber Users Data Database, Viber Users Thumbnails Cache, WBEM, WBEM, Webappstore, Webappstore XP, Webroot Program Data, WhatsApp Cache, WhatsApp Local Storage, Windows Defender Event Logs, Windows Defender Event Logs, Windows Defender Logs, Windows Defender Logs, Windows Defender Logs, Windows Defender Logs, Windows Protect Folder, Windows Protect Folder, Windows Protect Folder, WindowsIndexSearch, XML, XML, at .job, at .job, at SchedLgU.txt, at SchedLgU.txt, ccSubSDK Database, leveldb (Skype for Desktop +v8), mIRC Chat Logs (2000/XP), mIRC Chat Logs (Vista+), mRemoteNG Connection Configuration and Backups, mRemoteNG Logs, mRemoteNG Program Settings, main.db (App